@webjsdev/server 0.9.0 → 0.10.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 (101) hide show
  1. package/README.md +4 -1
  2. package/index.d.ts +427 -109
  3. package/index.js +8 -4
  4. package/package.json +3 -3
  5. package/src/action-flash.js +185 -0
  6. package/src/action-params.js +156 -0
  7. package/src/action-route.js +8 -9
  8. package/src/action-seed.js +44 -5
  9. package/src/actions.js +96 -14
  10. package/src/api.js +67 -10
  11. package/src/app-layout.js +133 -0
  12. package/src/asset-hash.js +82 -49
  13. package/src/auth.js +612 -108
  14. package/src/browser-entries.js +2 -17
  15. package/src/build-epoch.js +1 -1
  16. package/src/cache-tags.js +5 -1
  17. package/src/check/rules-actions.js +37 -1
  18. package/src/check/rules-components.js +114 -40
  19. package/src/check/rules-imports.js +175 -4
  20. package/src/check/rules-route-modules.js +200 -0
  21. package/src/check/rules-routes.js +55 -0
  22. package/src/check/rules-routing.js +143 -95
  23. package/src/check/rules-typescript.js +24 -2
  24. package/src/check/rules.js +104 -9
  25. package/src/check/runner-support.js +53 -171
  26. package/src/check/runner.js +22 -6
  27. package/src/client-signals.js +171 -0
  28. package/src/component-scanner.js +22 -16
  29. package/src/context.js +152 -16
  30. package/src/cookie-store.js +262 -0
  31. package/src/core-exports.js +190 -0
  32. package/src/dev/bun-app-source.js +95 -5
  33. package/src/dev/config.js +7 -51
  34. package/src/dev/dev-service-worker.js +48 -0
  35. package/src/dev/esm-hot-hooks.js +373 -0
  36. package/src/dev/handler.js +502 -155
  37. package/src/dev/helpers.js +127 -96
  38. package/src/dev/hot-host.js +33 -6
  39. package/src/dev/poke-state.js +110 -0
  40. package/src/dev/serve.js +177 -129
  41. package/src/dev/server.js +92 -33
  42. package/src/dev/specifier-versions.js +125 -0
  43. package/src/dev/watch-ignore.js +49 -4
  44. package/src/dev/watch-poll.js +78 -0
  45. package/src/dev/watch-tree.js +59 -22
  46. package/src/dev-classify.js +59 -26
  47. package/src/dev-error.js +104 -3
  48. package/src/dev-hot-classify.js +291 -0
  49. package/src/dev-hot-client.js +245 -0
  50. package/src/dev-import.js +22 -5
  51. package/src/dev-overlay.js +26 -0
  52. package/src/dev-ready-probe.js +69 -0
  53. package/src/dev-regenerate.js +2 -2
  54. package/src/dev-reload-hold.js +49 -20
  55. package/src/dev-reload-reason.js +61 -0
  56. package/src/dev-reload-worker.js +62 -13
  57. package/src/dev-secret.js +73 -0
  58. package/src/dev-source-locations-hook.js +4 -1
  59. package/src/dev-source-locations.js +33 -2
  60. package/src/dev.js +0 -1
  61. package/src/env-files.js +124 -0
  62. package/src/file-storage.js +2 -2
  63. package/src/foreign-imports.js +228 -0
  64. package/src/form-action-identity.js +52 -25
  65. package/src/form-dispatch.js +140 -65
  66. package/src/html-cache.js +35 -4
  67. package/src/importmap.js +2 -2
  68. package/src/instrumentation.js +59 -3
  69. package/src/js-scan.js +5 -4
  70. package/src/jwe.js +155 -0
  71. package/src/lazy-components.js +166 -0
  72. package/src/listener-core.js +4 -2
  73. package/src/lit-hints.js +172 -0
  74. package/src/middleware-load.js +248 -0
  75. package/src/module-graph.js +73 -47
  76. package/src/module-side-effect.js +284 -0
  77. package/src/public-files.js +73 -0
  78. package/src/redirect-location.js +19 -0
  79. package/src/redirects.js +9 -1
  80. package/src/route-frontier.js +151 -0
  81. package/src/route-report.js +130 -0
  82. package/src/router.js +120 -13
  83. package/src/session.js +32 -10
  84. package/src/ssr/client-router-flag.js +1 -1
  85. package/src/ssr/document.js +37 -13
  86. package/src/ssr/head.js +78 -23
  87. package/src/ssr/preloads.js +31 -45
  88. package/src/ssr/render.js +241 -55
  89. package/src/ssr/responses.js +51 -4
  90. package/src/ssr/shipped-modules.js +22 -16
  91. package/src/testing.d.ts +31 -2
  92. package/src/testing.js +80 -17
  93. package/src/thenable-params.js +26 -0
  94. package/src/vendor/jspm.js +12 -1
  95. package/src/vendor/pins.js +9 -8
  96. package/src/vendor/scanner.js +14 -9
  97. package/src/vendor.js +5 -3
  98. package/webjs-config.schema.json +1 -6
  99. package/src/component-elision.js +0 -1828
  100. package/src/elision-differential.js +0 -101
  101. package/src/elision-report.js +0 -193
@@ -29,7 +29,22 @@ export const RULES = [
29
29
  {
30
30
  name: 'no-server-env-in-components',
31
31
  description:
32
- 'Component files (under components/ or modules/*/components/) must not read non-public environment variables. process.env.X is allowed when X starts with WEBJS_PUBLIC_ (exposed to the browser via the SSR shim) or equals NODE_ENV (also defined in the browser). Any other process.env read in a component would leak the server-side value into the SSR\'d HTML, then read as undefined after hydration. Read server-only env vars in a page function, server action, or middleware (which never reach the browser as source) and pass derived values to the component as attributes.',
32
+ 'Code the browser runs must not read non-public environment variables: every component file (under components/ or modules/*/components/) and every module the build ships to the browser (a util a component imports). process.env.X is allowed when X starts with WEBJS_PUBLIC_ (exposed to the browser by the SSR shim) or equals NODE_ENV. Any other read leaks the server-side value into the SSR\'d HTML, then reads as undefined after hydration. Next.js\'s NEXT_PUBLIC_ prefix gets a targeted fix (rename it to WEBJS_PUBLIC_). Read server-only env vars in a page function, server action, or middleware (which never reach the browser as source) and pass derived values to the component as attributes. Page, layout and boundary modules are not scanned: their functions run on the server only.',
33
+ },
34
+ {
35
+ name: 'no-next-runtime-guard',
36
+ description:
37
+ 'Flags `process.env.NEXT_RUNTIME`, which only Next.js sets (to `nodejs` or `edge`). Under WebJs it is undefined, so the usual `if (process.env.NEXT_RUNTIME === \'nodejs\')` guard around an instrumentation `register()` silently never runs. Drop the guard (WebJs runs `register()` once, server-side only), and use `process.versions.bun` / `process.versions.node` to tell the runtimes apart.',
38
+ },
39
+ {
40
+ name: 'no-next-url',
41
+ description:
42
+ 'Flags `request.nextUrl` (as `req.nextUrl` / `request.nextUrl`, or any `.nextUrl` in a middleware or proxy file). It is Next.js\'s extended URL; WebJs passes middleware and route handlers the platform Request, so the property throws in dev. Use `new URL(request.url)`.',
43
+ },
44
+ {
45
+ name: 'no-inline-use-server',
46
+ description:
47
+ 'Flags a `\'use server\'` directive inside a function body (Next.js\'s inline server action, often written in a page). WebJs\'s server boundary is a FILE: the directive is honoured only at the top of a `*.server.{js,ts}` module, so an inline one is an ordinary string and the function runs wherever it is called, and binding it to a form throws (it has no action identity). Move the function into its own module (`modules/<feature>/actions/<name>.server.ts` starting with `\'use server\'`), export it, and import it.',
33
48
  },
34
49
  {
35
50
  name: 'tag-name-has-hyphen',
@@ -42,9 +57,9 @@ export const RULES = [
42
57
  'A custom-element tag name must be registered exactly once across the app. Two `Class.register(\'tag\')` / `customElements.define(\'tag\', …)` calls for the SAME tag resolve INCONSISTENTLY at runtime: SSR overwrites the registry (last registration wins) while the browser keeps the first native upgrade, so the rendered element and the webjs registry disagree. Rename one tag.',
43
58
  },
44
59
  {
45
- name: 'no-static-properties',
60
+ name: 'no-conflicting-routes',
46
61
  description:
47
- 'Reactive properties must be declared via the `extends WebComponent({ … })` factory, never a hand-written `static properties = { … }` field in the class body. The factory types each field for you (no `declare` needed) and the runtime throws on a direct `static properties`. Migrate `class X extends WebComponent { static properties = { count: { type: Number } } }` to `class X extends WebComponent({ count: Number })`; use the `prop()` helper for options (`prop(Number, { reflect: true })`) and set defaults in the constructor after super().',
62
+ 'Two files must not resolve to one URL: two pages (often in different route groups, `app/(marketing)/page.ts` beside `app/(app)/page.ts`, or `[id]` beside `[slug]` at one position), or a `page` and a `route` handler at the same path. Only one can ever be served, and Next.js refuses both at build time.',
48
63
  },
49
64
  {
50
65
  name: 'no-signal-field-assignment',
@@ -54,7 +69,7 @@ export const RULES = [
54
69
  {
55
70
  name: 'reactive-props-no-class-field',
56
71
  description:
57
- 'A reactive property declared via the `extends WebComponent({ … })` factory must NOT also have a plain class-field declaration (`count = 0`, `count: number = 0`, `count!: number`, or `count?: number`) in the class body. Under modern class-field semantics (including `erasableSyntaxOnly: true`) every class-field declaration compiles to Object.defineProperty *after* super(), clobbering the framework\'s reactive accessor and silently breaking re-renders. Set the default by assigning in the constructor after super().',
72
+ 'A reactive property declared via the `extends WebComponent({ … })` factory or Lit\'s `static properties` must NOT also have a plain class-field declaration (`count = 0`, `count: number = 0`, `count!: number`, or `count?: number`) in the class body. Under modern class-field semantics (including `erasableSyntaxOnly: true`) every class-field declaration compiles to Object.defineProperty *after* super(), clobbering the framework\'s reactive accessor and silently breaking re-renders. Set the default by assigning in the constructor after super(). A type-only `declare count: number` is erased and is fine.',
58
73
  },
59
74
  {
60
75
  name: 'array-prop-uses-array-type',
@@ -84,7 +99,7 @@ export const RULES = [
84
99
  {
85
100
  name: 'no-non-erasable-typescript',
86
101
  description:
87
- 'Scans .ts / .mts source for the four non-erasable TypeScript constructs (enum declarations, namespace blocks with value statements, constructor parameter properties, and `import = require`) that the framework\'s type-stripper rejects at request time. Companion to `erasable-typescript-only`: that rule checks the tsconfig flag, this rule checks the actual source. Both run by default so the flag check catches violations early in the editor while the source scan catches violations even if the tsconfig flag is missing or the rule is bypassed. Skips node_modules, dist, build, .git, .next, and _private folders.',
102
+ 'Scans .ts / .mts source for the four non-erasable TypeScript constructs (enum declarations, namespace blocks with value statements, constructor parameter properties, and `import = require`) that the framework\'s type-stripper rejects at request time, and .ts / .mts / .js / .mjs source for DECORATORS (`@customElement`, `@property`, `@state`, `@query`, any `@name` in decorator position). The stripper passes a decorator through untouched, so the module then fails to load with a bare `SyntaxError: Invalid or unexpected token` on Node and in the browser; the rule names the decorator and its WebJs spelling (`static properties` plus `declare`, or the `WebComponent({ ... })` factory, for `@property`; `state: true` for `@state`; `MyEl.register(tag)` for `@customElement`; a getter over `this.renderRoot` for `@query`). Comments, strings and template bodies are masked first, so `@click` in an html template, `@media` in a css template, and a JSDoc `@param` never match. Companion to `erasable-typescript-only`: that rule checks the tsconfig flag, this rule checks the actual source. Both run by default so the flag check catches violations early in the editor while the source scan catches violations even if the tsconfig flag is missing or the rule is bypassed. Skips node_modules, dist, build, .git, .next, _private folders, and .js files under public/.',
88
103
  },
89
104
  {
90
105
  name: 'no-browser-globals-in-render',
@@ -99,7 +114,7 @@ export const RULES = [
99
114
  {
100
115
  name: 'no-server-import-in-browser-module',
101
116
  description:
102
- 'A page / layout / component module that SHIPS to the browser (the build does NOT elide it) must not transitively import a server-only `.server.{ts,js}` module. The server module is replaced by a stub in the browser, so the import is fine while the module never loads client-side: a display-only page is elided, and a page whose only client relevance is importing shipping components is import-only (#605/#963), dropped from the boot in favour of its components. But the moment the page does its OWN client work (the client router, a reactive primitive, a module-scope browser-global access, a client-effecting non-component util) it ships whole, dragging the server import with it. The stub then throws (or a server-only export like `auth` is missing) the instant the module loads, a runtime browser crash that `webjs typecheck` and the rest of `webjs check` miss. The rule reuses the build\'s own elision verdict, so it ONLY fires on modules that genuinely ship; an elided, inert, or import-only page is never flagged. The fix is to keep the server call off the browser-shipped module: gate the route in `middleware.ts`, call the server through a `\'use server\'` action, or move the module\'s own client work into a component so the page is dropped again. Server-to-server imports (`.server.ts` importing `.server.ts`) and `middleware.ts` / `route.ts` (never shipped) are never flagged.',
117
+ 'A module that loads in the browser (a component outside a `.server` file, `instrumentation-client`, or anything either imports) must not transitively import a server-only `.server.{ts,js}` utility: in the browser that import is a stub that throws when the module loads, so the page crashes while typecheck passes. A `\'use server\'` action is fine (it becomes a working RPC stub). Pages, layouts and boundaries are never checked, because they run only on the server and never load in a browser (#1686), so a server import there is always fine. A component in a `.server` file never loads in the browser either, so it is not flagged.',
103
118
  },
104
119
  {
105
120
  name: 'one-action-per-configured-file',
@@ -112,9 +127,74 @@ export const RULES = [
112
127
  'Flags `<form action=${someAction}>` bound to a `\'use server\'` action whose file declares `export const method = \'GET\'` (#488). A GET action is a READ: it is CSRF-exempt and rides its arguments in the url, while a form submission is a CSRF-checked POST carrying a body, so the two contracts contradict each other and the submission is answered with a 405 at runtime. The rule reads the imported action\'s own file, so it only fires when the binding really does resolve to a GET-declared action. Fix by dropping the `method` export (an action with no `method` is a POST, which is what a form wants) or by binding a different action; if the form really is a read, use a plain `<form method="get" action="/search">` with no bound action.',
113
128
  },
114
129
  {
115
- name: 'no-redirect-in-api-route',
130
+ name: 'no-component-in-use-server-file',
131
+ description:
132
+ "Flags a component class in a `*.server.{ts,js}` file that starts with `'use server'`. Every function export of a `'use server'` file is wrapped as a server action, and a class is a function, so the component is neither a working component nor a usable action. Drop the directive to make it a server-only component (rendered on the server, never shipped), or move it to an ordinary file to make it a component that runs in the browser.",
133
+ },
134
+ {
135
+ name: 'no-interactivity-in-server-component',
136
+ description:
137
+ 'Flags a component in a `*.server.{ts,js}` file that uses an event binding, a reactive primitive (`signal`, `Task`, ...) or a client-only lifecycle hook (`connectedCallback`, `firstUpdated`, ...). A component in a `.server` file renders on the server only and is never defined in the browser, so that code never runs. Move it to an ordinary file, where a component always ships.',
138
+ },
139
+ {
140
+ name: 'no-server-component-in-client-template',
116
141
  description:
117
- 'API route handlers (`route.{js,ts}`) must NOT call `redirect()` from `@webjsdev/core`. That function throws a control-flow signal designed for the SSR page renderer; in a route handler it goes uncaught and produces a 500. Use `Response.redirect(url, 303)` for external redirects or return a 3xx Response directly. Page functions, layouts, and server actions may still use `redirect()` (caught by the SSR pipeline).',
142
+ 'Flags a module that loads in the browser (a component or anything it imports) rendering the tag of a component defined in a `*.server.{ts,js}` file. That tag is never defined in the browser, so it renders on the server and comes back empty after a client re-render, which is why Next.js does not let a client component render a server component. Render it from the page and pass it in as a child, where it lands in the component\'s `<slot>`.',
143
+ },
144
+ {
145
+ name: 'no-use-client-in-route-module',
146
+ description:
147
+ "Flags a `'use client'` directive at the top of a page, layout or boundary, the Next.js way to make a page interactive. In WebJs a route module is always a server component and the client part is a component, so the directive does nothing there. Move the interactive part into a component (`components/my-widget.ts`, Lit's APIs) and render its tag from the page. A `'use client'` in a component file is left alone, since a component already runs on the client.",
148
+ },
149
+ {
150
+ name: 'no-component-in-route-module',
151
+ description:
152
+ 'Flags a component class defined in a page, layout or boundary file. A route module runs only on the server and never loads in the browser (#1686), so the element renders its server markup and never upgrades. Move the class into its own file (`components/my-widget.ts`) and import it. Route-ness comes from the route table, so a helper that happens to be called `page.ts` outside `app/` is not a route module.',
153
+ },
154
+ {
155
+ name: 'no-event-binding-in-route-module',
156
+ description:
157
+ 'Flags an event binding (`@click=${...}`) on a tag in a page, layout or boundary template. A route module renders only on the server and never hydrates, so the handler never runs, the same reason Next.js refuses an event handler in a server component. Put the interactive part in a component, or use a `<form action=${action}>` or `<a href>` that works without JavaScript. A binding written as escaped text in a code sample (`&lt;button @click=...`) is not flagged.',
158
+ },
159
+ {
160
+ name: 'no-route-module-import-in-browser-module',
161
+ description:
162
+ 'Flags a module that loads in the browser (a component or anything it imports) importing a page, layout or boundary file. Route modules run only on the server and are never served to the browser (#1686), so the import 404s and the importing module fails to load. Move the shared code into its own file and import it from both.',
163
+ },
164
+ {
165
+ name: 'no-node-builtin-in-browser-module',
166
+ description:
167
+ 'Flags a `node:` import, or a bare Node builtin such as `fs`, in a module that loads in the browser (a component or anything it imports). It can never resolve there, so the module fails to load. Do the work in the page (pages run only on the server) and pass the result in as a property, or behind a `\'use server\'` action. An `import type` is erased and never flagged.',
168
+ },
169
+ {
170
+ name: 'no-foreign-framework-import',
171
+ description:
172
+ 'Flags an import of `next` / `next/*`, `react` / `react-dom`, `lit` / `lit-html` / `lit-element` / `lit/*`, `@lit/*` or `@lit-labs/*`, in any form (named, default, namespace, `import type`, side-effect, `export ... from`, or a string-literal dynamic `import()`). WebJs never aliases or loads those packages: a Next import fails to resolve, and a Lit import quietly loads a second component runtime from the CDN. The violation names the exact `@webjsdev/*` replacement (for example `next/navigation` -> `redirect` / `notFound` from `@webjsdev/core`, `lit` -> `WebComponent` / `html` / `css` from `@webjsdev/core`, `@lit/context` -> `@webjsdev/core/context`). `react` / `react-dom` stay allowed inside a `*.server.*` module (an email renderer, say), which never reaches the browser. The same refusal runs at load time: the dev overlay and production readiness name the import, and a server import is rewritten to the same fix.',
173
+ },
174
+ {
175
+ name: 'no-jsx-attributes',
176
+ description:
177
+ 'Flags JSX written into an `html` template: a `className=` or `htmlFor=` attribute (HTML reads them as unknown `classname` / `htmlfor` attributes, so the class never applies and the label never targets its control) and a capitalised component tag such as `<Link>` or `<Button>` (HTML lowercases it into an unknown element that renders nothing). Both renderers throw on these when the template first compiles; the rule reports them ahead of time. Write `class=` / `for=`, and render a custom element by its registered hyphenated tag (or a plain `<a href>` for a link). Text in the template (`&lt;Link&gt;`, a `className` mentioned in prose), comments, and script / style bodies are never flagged.',
178
+ },
179
+ {
180
+ name: 'no-jsx-route-file',
181
+ description:
182
+ 'Flags a routing file (`page`, `layout`, `route`, a boundary such as `error` / `not-found` / `loading`, `middleware`, or a metadata route) written as `.tsx` or `.jsx`, the Next.js default. WebJs has no build step, so JSX never loads and the router skips the file: the route silently does not exist. Production refuses to boot on it, dev answers every request with the problem, and `webjs doctor` fails (`JSX_ROUTE_FILES`). Rename it to `.ts` / `.js` and return an `html` template.',
183
+ },
184
+ {
185
+ name: 'no-css-import',
186
+ description:
187
+ 'Flags `import \'./styles.css\'` (or any `.css` import, including `import styles from \'./x.module.css\'`). Importing CSS is a bundler feature WebJs does not have: Node refuses the `.css` extension, Bun silently turns the import into a no-op on the server, and the browser refuses a stylesheet as a module script, so the importing module never loads. The same refusal runs at load time. Load a stylesheet with `<link rel="stylesheet" href="...">` in the root layout (the scaffold links its Tailwind output this way), or scope CSS to a component with `static styles = css` and `static shadow = true`.',
188
+ },
189
+ {
190
+ name: 'no-public-route-conflict',
191
+ description:
192
+ 'Files in `public/` are served at the URL ROOT, as in Next.js (`public/logo.png` is `/logo.png`; there is no `/public/` prefix), and before every route, so a public file wins over a dynamic route. A public file at the same URL as a static page, a `route.{js,ts}` handler, or a metadata route (`public/robots.txt` beside `app/robots.ts`) would make one of them unreachable, so it is flagged here and refused at production boot. Rename or move one side.',
193
+ },
194
+ {
195
+ name: 'no-page-route-conflict',
196
+ description:
197
+ 'A `page.{js,ts}` and a `route.{js,ts}` must not answer the same URL (including through a route group, `app/(a)/x/page.ts` beside `app/x/route.ts`). Next.js refuses this at build time; WebJs refuses to boot in production and answers every dev request with the problem, because the route handler would shadow the page and it could never render. Move the route handler to its own folder (`app/api/...`) or delete the page.',
118
198
  },
119
199
  {
120
200
  name: 'no-interpolation-in-raw-text-element',
@@ -124,7 +204,22 @@ export const RULES = [
124
204
  {
125
205
  name: 'no-missing-local-import',
126
206
  description:
127
- 'Flags a NAMED value import of a symbol that a resolvable app-internal module does not export (`import { todos } from \'#db/schema.server.ts\'` when the schema no longer exports `todos`). The binding is `undefined` at runtime and crashes on first use, yet the rest of `webjs check` (elision-based, it does not compile types) misses it, so a schema swap that orphans a gallery module can pass `check` while `typecheck` is red. This is the runtime-crash class `check` exists to catch, filling that gap so an agent running only `check` cannot ship a broken import. Deliberately conservative to never false-positive on a valid app: it only inspects app-internal specifiers (a relative `./` path or a `#` alias) that resolve to a file in the app, only NAMED value imports (a `type` import, a default import, a namespace import, and any bare / `node:` / npm-package specifier are skipped), and it treats a module as UNKNOWABLE (never flags imports from it) when its exports are not fully enumerable: a `export * from ...` star re-export, a destructuring `export const { a } = ...`, or a multi-declarator `export const a = .., b = ..`. It reads names from `export function/class/const/let/var/type/interface/enum` and from `export { a, b as c }` / `export { x } from ...` clauses (the alias after `as` is the exported name). A re-export barrel therefore resolves correctly, and a `\'use server\'` action file exports its function names normally. Found dogfooding a tic-tac-toe app: dropping the example `todos` table left a gallery module importing it, green under `check` but red under `typecheck`.',
207
+ 'Flags a NAMED value import of a symbol that a resolvable app-internal module does not export (`import { todos } from \'#db/schema.server.ts\'` when the schema no longer exports `todos`). The binding is `undefined` at runtime and crashes on first use, yet the rest of `webjs check` (graph-based, it does not compile types) misses it, so a schema swap that orphans a gallery module can pass `check` while `typecheck` is red. This is the runtime-crash class `check` exists to catch, filling that gap so an agent running only `check` cannot ship a broken import. Deliberately conservative to never false-positive on a valid app: it only inspects app-internal specifiers (a relative `./` path or a `#` alias) that resolve to a file in the app, only NAMED value imports (a `type` import, a default import, a namespace import, and any bare / `node:` / npm-package specifier are skipped), and it treats a module as UNKNOWABLE (never flags imports from it) when its exports are not fully enumerable: a `export * from ...` star re-export, a destructuring `export const { a } = ...`, or a multi-declarator `export const a = .., b = ..`. It reads names from `export function/class/const/let/var/type/interface/enum` and from `export { a, b as c }` / `export { x } from ...` clauses (the alias after `as` is the exported name). A re-export barrel therefore resolves correctly, and a `\'use server\'` action file exports its function names normally. Found dogfooding a tic-tac-toe app: dropping the example `todos` table left a gallery module importing it, green under `check` but red under `typecheck`.',
208
+ },
209
+ {
210
+ name: 'static-styles-need-shadow',
211
+ description:
212
+ 'Flags a class extending `WebComponent` that declares `static styles` without `static shadow = true`. WebJs adopts `static styles` into a SHADOW root, and a component renders into light DOM unless it opts into shadow DOM (the reverse of Lit\'s default), so the styles are never applied. Fix: add `static shadow = true` to scope them, or drop `static styles` and style the light DOM with Tailwind utilities or a global stylesheet whose class selectors are prefixed with the tag name. A `static shadow` the rule cannot evaluate (a getter or an expression) is not flagged.',
213
+ },
214
+ {
215
+ name: 'shadow-options-need-shadow',
216
+ description:
217
+ 'Flags a class extending `WebComponent` that declares `static shadowRootOptions` (Lit\'s `{ mode, delegatesFocus, ... }` for `attachShadow`) without `static shadow = true`. The options configure a shadow root, and the component has none: it renders into light DOM by default, so they are ignored. Fix: add `static shadow = true`, or remove the options.',
218
+ },
219
+ {
220
+ name: 'core-imports-exist',
221
+ description:
222
+ 'Flags a named value import from `@webjsdev/core` (or one of its subpaths) of a name that module does not export, a default import from a module with no default export, and an import from a subpath core does not export. Each is a load-time crash: ESM refuses to link the module with `does not provide an export named X`, which names neither the cause nor the fix. Lit habits produce it: `LitElement` (use `WebComponent`), a decorator function such as `customElement` or `property` (use `MyEl.register(tag)` and `static properties`), or a type such as `PropertyValues` or `TemplateResult` imported as a value (use `import type`, since the type stripper keeps a plain import). The rule reads the INSTALLED core package\'s real export surface (each `exports` subpath\'s source module for values, its declaration file for types), so a name core gains is accepted as soon as it ships, and a name exported by a different subpath is pointed there. `import type` imports and `type`-qualified names are skipped, since they are erased before load.',
128
223
  },
129
224
  ];
130
225
 
@@ -6,12 +6,12 @@
6
6
  * and runner.js calls the rules. Parked in runner.js it made that a cycle.
7
7
  */
8
8
  import { readFile, stat } from 'node:fs/promises';
9
- import { join, relative, basename } from 'node:path';
9
+ import { join, relative } from 'node:path';
10
10
  import { walk } from '../fs-walk.js';
11
11
  import { buildModuleGraph, transitiveDeps } from '../module-graph.js';
12
12
  import { scanComponents } from '../component-scanner.js';
13
13
  import { buildRouteTable } from '../router.js';
14
- import { analyzeElision } from '../component-elision.js';
14
+ import { routeTableFiles } from '../route-frontier.js';
15
15
  import {
16
16
  hasUseServerDirective,
17
17
  } from './helpers.js';
@@ -60,70 +60,53 @@ export function findImportChain(graph, from, to) {
60
60
  * Implements `no-server-import-in-browser-module`. Factored into its own
61
61
  * function (rather than an inline block) because it does the heavier
62
62
  * whole-app analysis the other rules avoid: it builds the module graph,
63
- * scans components, builds the route table, and runs the framework's own
64
- * elision analysis so the rule's notion of "ships to the browser" is
65
- * byte-for-byte the build's.
63
+ * scans components and builds the route table, and returns them for the
64
+ * rules that share the analysis.
66
65
  *
67
66
  * A module is flagged when BOTH hold:
68
- * 1. It SHIPS to the browser. For a component that means it is NOT in the
69
- * elidable set; for a page / layout that means it is NOT in the inert
70
- * route-module set. (Pages and layouts that do real client work are not
71
- * inert and therefore ship.)
72
- * 2. Its transitive import closure reaches a `.server.{ts,js}` module.
73
- * `transitiveDeps` stops AT a server file (it is included but not walked
74
- * into), so a server file pulled in only through another server file is
75
- * not attributed to a browser module that never reaches it directly.
67
+ * 1. It loads in the browser: a component (every component in an ordinary
68
+ * file ships, as in Lit) or `instrumentation-client.*`. A route module
69
+ * (page, layout, boundary) never ships (#1686), so a server import in
70
+ * one is always fine.
71
+ * 2. Its transitive import closure reaches a `.server.{ts,js}` utility
72
+ * (no `'use server'`). `transitiveDeps` stops AT a server file (it is
73
+ * included but not walked into), so a server file pulled in only through
74
+ * another server file is not attributed to a browser module that never
75
+ * reaches it directly.
76
76
  *
77
- * Also flagged: error / loading / not-found modules. These ship to the browser
78
- * too (the dev server's `computeBrowserBoundFiles` adds them unconditionally)
79
- * and are never elided, so a server import reaching one of them is the same
80
- * throw-at-load crash.
81
- *
82
- * Never flagged: a `.server.ts` importing another `.server.ts` (server-to-
83
- * server, and `.server.*` modules are not components nor route modules), and
84
- * `middleware.ts` / `route.ts` (server-only, never page/layout/component
85
- * entries, so they are not in the candidate set to begin with).
77
+ * Never flagged: a `.server.ts` importing another `.server.ts` (server to
78
+ * server; a `.server.*` file is never a scanned component, so a component in
79
+ * one is the server-only kind and is not a candidate), and `middleware.ts` /
80
+ * `route.ts` (server-only, not in the candidate set to begin with).
86
81
  *
87
82
  * Scope note for dynamic imports: a string-literal `import('./widget.ts')` IS
88
- * tracked by the module graph now (#751), but only as a GATE edge (so the
89
- * lazily-imported module is servable); this rule's server-import detection
90
- * still runs over STATIC edges only (`transitiveDeps`), so a dynamic
91
- * `import('./x.server.ts')` of a no-`'use server'` utility is not flagged here.
92
- * That is deliberate: the throw-at-load crash is deferred to call time (when
93
- * the module is actually fetched), not module load, and a dynamic import is
94
- * also not elided framework-wide. A computed `import(expr)` cannot be resolved
95
- * statically at all; rather than a false-positive-prone check rule (a computed
96
- * import of an npm specifier or an otherwise-reachable app module is perfectly
97
- * valid, so it fails the check-is-correctness-only dividing line), the dev
98
- * server surfaces it with a 404 hint when the target 404s (see dev.js #751).
83
+ * tracked by the module graph (#751), but only as a GATE edge (so the lazily
84
+ * imported module is servable); this rule's server-import detection runs over
85
+ * STATIC edges only (`transitiveDeps`), so a dynamic `import('./x.server.ts')`
86
+ * of a no-`'use server'` utility is not flagged here. That is deliberate: the
87
+ * throw-at-load crash is deferred to call time. A computed `import(expr)`
88
+ * cannot be resolved statically at all; rather than a false-positive-prone
89
+ * check rule, the dev server surfaces it with a 404 hint when the target 404s
90
+ * (see dev.js #751).
99
91
  *
100
92
  * @param {string} appDir
101
93
  * @param {Violation[]} violations appended to in place
102
94
  */
103
95
  // --- Rule: no-server-import-in-browser-module ---
104
- // A page / layout / component module that SHIPS to the browser must not
105
- // transitively import a server-only `.server.{ts,js}` module. The browser
106
- // gets a stub for the server file, so the import is harmless while the
107
- // module never loads client-side: a display-only page is elided, and an
108
- // import-only page (#605/#963) is dropped from the boot in favour of its
109
- // components. But the moment the page does its OWN client work (the client
110
- // router, a reactive primitive, module-scope code, a client-effecting util
111
- // on a component-free path) it ships whole, must load in the browser,
112
- // and drags the server import with it: the stub throws (or a server-only
113
- // export like `auth` is missing) the instant the module loads. That crash
114
- // only surfaces at runtime; typecheck and every other check pass.
115
- //
116
- // The rule reuses the BUILD'S elision verdict (analyzeElision) instead of
117
- // re-deriving it, so it fires ONLY on modules that genuinely ship: a
118
- // display-only page the framework elides is never flagged (that is the
119
- // legitimate pattern). The motivating case (crisp dogfood): a page that does
120
- // `await auth()` (import from `lib/auth.server.ts`) AND imports a component
121
- // directly, so it is not elided and ships the server import.
96
+ // A module the browser loads must not transitively import a server-only
97
+ // `.server.{ts,js}` utility. The browser gets a stub for the server file, and
98
+ // the stub throws (or a server-only export like `auth` is missing) the
99
+ // instant the module loads. That crash only surfaces at runtime; typecheck
100
+ // and every other check pass. A `'use server'` action is fine: its stub is a
101
+ // working RPC call.
122
102
  export async function checkServerImportInBrowserModule(appDir, violations) {
103
+ /** Every non-server module the browser loads (the shipping candidates and
104
+ * their transitive closures), returned for the env rule. @type {Set<string>} */
105
+ const shipped = new Set();
123
106
  // No `app/` directory means this is not a routable WebJs app (e.g. a bare
124
107
  // component library, or a fixture with only `lib/`); nothing ships, so the
125
108
  // rule has nothing to police. Skip rather than do the heavy analysis.
126
- if (!(await pathExists(join(appDir, 'app')))) return;
109
+ if (!(await pathExists(join(appDir, 'app')))) return { shipped, routeFiles: new Set(), componentFiles: new Set(), moduleGraph: new Map() };
127
110
 
128
111
  let moduleGraph, components, routeTable;
129
112
  try {
@@ -133,91 +116,24 @@ export async function checkServerImportInBrowserModule(appDir, violations) {
133
116
  } catch {
134
117
  // A malformed app the analysis can't process is left to the other rules
135
118
  // (and the dev server) to surface; this rule degrades to a no-op.
136
- return;
137
- }
138
-
139
- // Page + layout modules that the router treats as route modules, exactly the
140
- // set the dev server feeds to analyzeElision (so the inert verdict matches).
141
- /** @type {Set<string>} */
142
- const routeModuleSet = new Set();
143
- for (const page of routeTable.pages || []) {
144
- if (page.file) routeModuleSet.add(page.file);
145
- for (const f of page.layouts || []) routeModuleSet.add(f);
146
- }
147
- const routeModules = [...routeModuleSet];
148
-
149
- // error / loading / not-found modules ALSO ship to the browser, but unlike
150
- // pages + layouts they are never elided: the dev server's
151
- // `computeBrowserBoundFiles` adds them to the browser-bound entry set
152
- // unconditionally (only ELIDABLE-COMPONENT imports are ever stripped, and
153
- // these modules have no component to strip). So a personalized 404 that does
154
- // `await auth()` is a real throw-at-load crash the page+layout-only candidate
155
- // set would miss. Collect them here and add them to the candidate set as
156
- // always-shipping (no elision verdict to consult).
157
- /** @type {Map<string, string>} abs file -> kind */
158
- const alwaysShipRouteModules = new Map();
159
- for (const page of routeTable.pages || []) {
160
- for (const f of page.errors || []) alwaysShipRouteModules.set(f, 'error boundary');
161
- for (const f of page.loadings || []) alwaysShipRouteModules.set(f, 'loading boundary');
162
- // forbidden / unauthorized joined this set with #1298. They used to render
163
- // with an empty boot set, so their modules never reached the browser and
164
- // omitting them here was harmless. They now render inside their layout
165
- // chain AND ship, so a `forbidden.ts` doing `import { auth } from
166
- // '#lib/auth.server.ts'` (a natural thing on a 403: "signed in as X, but
167
- // not permitted") is a real throw-at-load crash that takes every sibling
168
- // registration in the same module script with it.
169
- for (const f of page.forbiddens || []) alwaysShipRouteModules.set(f, 'forbidden boundary');
170
- for (const f of page.unauthorizeds || []) alwaysShipRouteModules.set(f, 'unauthorized boundary');
171
- }
172
- if (routeTable.notFound) alwaysShipRouteModules.set(routeTable.notFound, 'not-found page');
173
- if (routeTable.notFounds) {
174
- for (const f of routeTable.notFounds.values()) {
175
- alwaysShipRouteModules.set(f, 'not-found page');
176
- }
177
- }
178
- // global-not-found ships for the same reason: a thrown notFound() with no
179
- // nearer boundary renders it against the matched route, so it wraps and
180
- // boots. global-ERROR is deliberately absent: it returns its own document
181
- // verbatim with no importmap and no boot script, so nothing of it ever
182
- // reaches the browser.
183
- if (routeTable.globalNotFound) {
184
- alwaysShipRouteModules.set(routeTable.globalNotFound, 'global-not-found page');
119
+ return { shipped, routeFiles: new Set(), componentFiles: new Set(), moduleGraph: new Map() };
185
120
  }
186
121
 
187
- // The elision flag mirrors `dev.js`: respect `webjs.elide === false` and the
188
- // WEBJS_ELIDE override. When elision is OFF, the build ships EVERY component
189
- // and route module, so the verdict is "nothing is elidable / inert" and the
190
- // rule treats every candidate as shipping (which is correct: with elision
191
- // off, a display-only page really does ship its server import too).
192
- const elideEnabled = await readElideEnabledForCheck(appDir);
193
- const { elidableComponents, inertRouteModules, importOnlyRouteModules } = elideEnabled
194
- ? await analyzeElision(components, routeModules, moduleGraph, (f) => readFile(f, 'utf8'), appDir)
195
- : { elidableComponents: new Set(), inertRouteModules: new Set(), importOnlyRouteModules: new Map() };
122
+ // What the browser loads (#1686): every component, as in Lit, plus
123
+ // `instrumentation-client.*`. A route module (page, layout, boundary) never
124
+ // ships, so a server import in one is fine wherever it sits. A `.server.*`
125
+ // file is never a scanned component (a component there is the server-only
126
+ // kind), so server-to-server imports are excluded by construction.
127
+ const routeFiles = routeTableFiles(routeTable);
196
128
 
197
- // Candidate browser-shipped modules: components that are NOT elided, plus
198
- // route modules that are NOT inert and NOT import-only (an import-only
199
- // module is dropped from the boot in favour of its component frontier,
200
- // #605/#963, so its own imports never load in a browser and a bare
201
- // server-only import in it is harmless). A `.server.*` file is never a
202
- // component (the scanner skips it) nor a route module the browser loads, so
203
- // it cannot enter this set; server-to-server imports are excluded by
204
- // construction.
205
- /** @type {Map<string, { kind: string }>} relFile is keyed by ABS path */
129
+ /** @type {Map<string, { kind: string }>} keyed by ABS path */
206
130
  const candidates = new Map();
207
131
  for (const c of components) {
208
- if (!elidableComponents.has(c.file)) candidates.set(c.file, { kind: 'component' });
209
- }
210
- for (const file of routeModules) {
211
- if (inertRouteModules.has(file) || importOnlyRouteModules.has(file)) continue;
212
- const base = basename(file);
213
- const kind = /^layout\./.test(base) ? 'layout' : 'page';
214
- candidates.set(file, { kind });
132
+ if (routeFiles.has(c.file)) continue;
133
+ candidates.set(c.file, { kind: 'component' });
215
134
  }
216
- // error / loading / not-found modules always ship (never elided), so they are
217
- // candidates unconditionally. A page/layout entry already in `candidates`
218
- // wins (it is the more specific kind); these only add files not already seen.
219
- for (const [file, kind] of alwaysShipRouteModules) {
220
- if (!candidates.has(file)) candidates.set(file, { kind });
135
+ if (routeTable.instrumentationClient) {
136
+ candidates.set(routeTable.instrumentationClient, { kind: 'instrumentation-client module' });
221
137
  }
222
138
 
223
139
  // Report at most once per module (a page importing two server modules is one
@@ -228,6 +144,8 @@ export async function checkServerImportInBrowserModule(appDir, violations) {
228
144
  // is not in the result. A direct OR indirect server import both surface,
229
145
  // because the closure walks every non-server edge until it hits one.
230
146
  const closure = transitiveDeps(moduleGraph, [file], appDir);
147
+ shipped.add(file);
148
+ for (const d of closure) if (!/\.server\.m?[jt]s$/.test(d)) shipped.add(d);
231
149
  // Of the reachable server files, find one that is a genuine throw-at-load
232
150
  // crash in the browser. TWO kinds of `.server.*` import are NOT crashes and
233
151
  // must be skipped, or the rule false-positives on legitimate code:
@@ -265,44 +183,23 @@ export async function checkServerImportInBrowserModule(appDir, violations) {
265
183
  const importer = chainFiles.length >= 2 ? chainFiles[chainFiles.length - 2] : null;
266
184
  const viaTypesModule = importer && /(^|\/)types(\.m?[jt]s$|\/)/.test(relative(appDir, importer));
267
185
 
268
- // The "elides again" remedy only applies to a page / layout: since the
269
- // path-aware import-only verdict (#963), a page importing a component to
270
- // register is dropped from the boot, so a page/layout on this rule ships
271
- // because of its OWN client work (or a client-effecting non-component in
272
- // its closure); moving that work into a component makes it a dropped
273
- // carrier again and the server import never loads. The error / loading /
274
- // not-found boundaries always ship and are never elided, so offering
275
- // them an "elides again" fix is wrong. Branch the fix text on kind.
276
- const canElide = kind === 'page' || kind === 'layout';
277
186
  const typesHint = viaTypesModule
278
187
  ? `The edge enters via a types-shaped module (${relative(appDir, importer)}); if it re-exports a runtime VALUE from a \`.server.{ts,js}\` file, relocate that to a browser-safe typedef (a plain \`interface\` / JSDoc, or an \`import type\` which the stripper erases) so the type is shared without pinning the module. `
279
188
  : '';
280
- const fixText = canElide
281
- ? `${typesHint}Keep the server call off this browser-shipped ${kind}. Options: (1) gate the route in \`middleware.ts\` (runs server-side, never ships); (2) move the server-only call behind a \`'use server'\` action in a \`.server.{ts,js}\` file and call it as an RPC; or (3) move this ${kind}'s own client work (the module-scope call, browser-global access, or client-effecting util import that pins it) into a component, so the ${kind} elides again as a dropped carrier and its server import never loads.`
282
- : `${typesHint}Keep the server call off this browser-shipped ${kind} (it always ships and is never elided). Options: (1) gate the route in \`middleware.ts\` (runs server-side, never ships); or (2) move the server-only call behind a \`'use server'\` action in a \`.server.{ts,js}\` file and call it as an RPC.`;
189
+ const fixText = `${typesHint}Keep the server call out of this ${kind}. Load the data in the page (pages run only on the server) and pass it in as a property, or move the call behind a \`'use server'\` action in a \`.server.{ts,js}\` file and call it as an RPC. A component that only displays server data can instead live in a \`*.server.ts\` file (without \`'use server'\`): it then renders on the server, may import server code, and ships nothing, like a Next.js server component.`;
190
+ const whyText = '';
283
191
 
284
192
  violations.push({
285
193
  rule: 'no-server-import-in-browser-module',
286
194
  file: relFile,
287
195
  message:
288
- `This ${kind} ships to the browser (the build does not elide it) but transitively imports the server-only module ${relServer} (${chain}). In the browser that import resolves to a stub, so the module crashes at load (the stub throws, or a server-only export such as \`auth\` is missing). \`webjs typecheck\` and the rest of \`webjs check\` pass; only the running ${kind} fails.`,
196
+ `This ${kind} loads in the browser but transitively imports the server-only module ${relServer} (${chain}).${whyText} In the browser that import resolves to a stub, so the module crashes at load (the stub throws, or a server-only export such as \`auth\` is missing). \`webjs typecheck\` and the rest of \`webjs check\` pass; only the running ${kind} fails.`,
289
197
  fix: fixText,
290
198
  });
291
199
  }
200
+ return { shipped, routeFiles, componentFiles: new Set(components.map((c) => c.file)), moduleGraph };
292
201
  }
293
202
 
294
- /**
295
- * Read whether component elision is enabled for `appDir`, mirroring
296
- * `dev.js`'s `readElideEnabled` so the check's notion of "ships" matches the
297
- * dev server's. Elision is ON unless `webjs.elide === false` in package.json or
298
- * the `WEBJS_ELIDE` env var forces it off (`0` / `false` / `off` / `no`). A
299
- * missing or malformed package.json keeps the default (on). Inlined rather
300
- * than imported from `dev.js` so the check tool does not pull the whole dev
301
- * server module just for this flag.
302
- *
303
- * @param {string} appDir
304
- * @returns {Promise<boolean>}
305
- */
306
203
  /**
307
204
  * True if `file` is a `'use server'` action: a `.server.{ts,js}` module that
308
205
  * declares the `'use server'` directive. The dev server rewrites its browser
@@ -325,21 +222,6 @@ export async function isUseServerActionFile(file) {
325
222
  }
326
223
  }
327
224
 
328
- export async function readElideEnabledForCheck(appDir) {
329
- const raw = process.env.WEBJS_ELIDE;
330
- if (raw != null) {
331
- const v = raw.trim().toLowerCase();
332
- if (v === '0' || v === 'false' || v === 'off' || v === 'no') return false;
333
- if (v === '1' || v === 'true' || v === 'on' || v === 'yes') return true;
334
- }
335
- try {
336
- const pkg = JSON.parse(await readFile(join(appDir, 'package.json'), 'utf8'));
337
- if (pkg && pkg.webjs && pkg.webjs.elide === false) return false;
338
- } catch {
339
- // No package.json, malformed JSON, or unreadable: keep the default.
340
- }
341
- return true;
342
- }
343
225
 
344
226
  /**
345
227
  * Async fs.exists shim. Returns true if the path exists at all (file
@@ -6,18 +6,21 @@ import {
6
6
  } from '../js-scan.js';
7
7
  import {
8
8
  checkComponentsHaveRegister,
9
- checkNoStaticProperties,
10
9
  checkNoSignalFieldAssignment,
11
10
  checkReactivePropsNoClassField,
12
11
  checkArrayPropUsesArrayType,
13
12
  checkNoBrowserGlobalsInRender,
14
13
  checkNoShadowedNativeMember,
14
+ checkShadowOnlyStatics,
15
15
  } from './rules-components.js';
16
16
  import {
17
- checkNoRedirectInApiRoute,
18
17
  checkNoInterpolationInRawTextElement,
19
18
  checkNoServerEnvInComponents,
19
+ checkNoNextRuntimeGuard,
20
+ checkNoNextUrl,
20
21
  checkShellInNonRootLayout,
22
+ checkAppLayout,
23
+ checkNoJsxAttributes,
21
24
  } from './rules-routing.js';
22
25
  import {
23
26
  checkErasableTypescriptOnly,
@@ -27,15 +30,20 @@ import {
27
30
  checkUseServerNeedsExtension,
28
31
  checkUseServerExportsCallable,
29
32
  checkOneActionPerConfiguredFile,
33
+ checkNoInlineUseServer,
30
34
  } from './rules-actions.js';
31
35
  import {
32
36
  checkTagNameHasHyphen,
33
37
  checkNoDuplicateTag,
34
38
  } from './rules-registry.js';
35
39
  import { checkServerImportInBrowserModule } from './runner-support.js';
40
+ import { checkRouteModuleBoundaries } from './rules-route-modules.js';
41
+ import { checkNoConflictingRoutes } from './rules-routes.js';
36
42
  import {
37
43
  checkNoMissingLocalImport,
38
44
  checkFormActionNotAGetAction,
45
+ checkCoreImportsExist,
46
+ checkNoForeignFrameworkImport,
39
47
  } from './rules-imports.js';
40
48
 
41
49
  /**
@@ -90,26 +98,34 @@ export async function checkConventions(appDir) {
90
98
  }
91
99
 
92
100
  checkComponentsHaveRegister(files, violations);
93
- checkNoStaticProperties(files, violations);
94
101
  checkNoSignalFieldAssignment(files, violations);
95
102
  checkReactivePropsNoClassField(files, violations);
96
103
  checkArrayPropUsesArrayType(files, violations);
97
104
  checkNoBrowserGlobalsInRender(files, violations);
98
105
  checkNoShadowedNativeMember(files, violations);
99
- checkNoRedirectInApiRoute(files, violations);
106
+ checkShadowOnlyStatics(files, violations);
100
107
  checkNoInterpolationInRawTextElement(files, violations);
101
- checkNoServerEnvInComponents(files, violations);
102
108
  checkShellInNonRootLayout(files, violations);
103
109
  await checkErasableTypescriptOnly(appDir, violations);
104
110
  await checkNoNonErasableTypescript(appDir, violations);
105
111
  checkUseServerNeedsExtension(files, violations);
106
112
  checkUseServerExportsCallable(files, violations);
107
113
  checkOneActionPerConfiguredFile(files, violations);
114
+ checkNoInlineUseServer(files, violations);
108
115
  checkTagNameHasHyphen(files, violations);
109
116
  await checkNoDuplicateTag(appDir, files, violations);
110
- await checkServerImportInBrowserModule(appDir, violations);
117
+ await checkNoConflictingRoutes(appDir, violations);
118
+ const browser = await checkServerImportInBrowserModule(appDir, violations);
119
+ checkNoServerEnvInComponents(files, violations, browser.shipped, browser.routeFiles);
120
+ checkRouteModuleBoundaries(appDir, files, violations, browser);
121
+ checkNoNextRuntimeGuard(files, violations);
122
+ checkNoNextUrl(files, violations);
111
123
  checkNoMissingLocalImport(appDir, files, violations);
112
124
  checkFormActionNotAGetAction(appDir, files, violations);
125
+ checkCoreImportsExist(appDir, files, violations);
126
+ checkNoForeignFrameworkImport(files, violations);
127
+ checkNoJsxAttributes(files, violations);
128
+ await checkAppLayout(appDir, violations);
113
129
 
114
130
  return violations;
115
131
  }