@webjsdev/core 0.7.46 → 0.7.47

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/index-browser.js CHANGED
@@ -31,6 +31,10 @@ export { escapeText, escapeAttr } from './src/escape.js';
31
31
  export { notFound, redirect, forbidden, unauthorized, isNotFound, isRedirect, isForbidden, isUnauthorized } from './src/nav.js';
32
32
  export { cspNonce } from './src/csp-nonce.js';
33
33
  export { asset } from './src/asset-url.js';
34
+ // Form actions (#1155). The field name is shared with the server dispatcher;
35
+ // `setFormActionResolver` is server-only wiring and is stripped here, because
36
+ // a browser stub carries its own identity and needs no resolver.
37
+ export { FORM_ACTION_FIELD, FORM_ACTION_ID_KEY } from './src/form-action.js';
34
38
  export { repeat, isRepeat } from './src/repeat.js';
35
39
  export { Suspense, isSuspense } from './src/suspense.js';
36
40
  export { connectWS } from './src/websocket-client.js';
package/index.d.ts CHANGED
@@ -52,6 +52,8 @@ export type {
52
52
  WebjsRedirectRule,
53
53
  WebjsTrailingSlash,
54
54
  WebjsCspConfig,
55
+ WebjsDoctorConfig,
56
+ WebjsDoctorSeverity,
55
57
  } from './src/webjs-config.d.ts';
56
58
 
57
59
  // Compile-time serializability typing for server actions (#488): the opt-in
@@ -105,6 +107,7 @@ export { Task, TaskStatus } from './src/task.js';
105
107
  export { signal, computed, effect, batch, isSignal, Signal } from './src/signal.js';
106
108
  export { cspNonce, setCspNonceProvider } from './src/csp-nonce.js';
107
109
  export { asset, setAssetUrlProvider } from './src/asset-url.js';
110
+ export { FORM_ACTION_FIELD, FORM_ACTION_ID_KEY, setFormActionResolver } from './src/form-action.js';
108
111
  export { takeSeed, scanSeeds, SEED_MISS } from './src/action-seed-client.js';
109
112
  export { markStale, registerKeyTags, consumeStale, parseTagHeader, fetchMark } from './src/action-cache-client.js';
110
113
  // Client action-abort plumbing (#492): a superseded async render aborts its
package/index.js CHANGED
@@ -16,6 +16,10 @@ export { escapeText, escapeAttr } from './src/escape.js';
16
16
  export { notFound, redirect, forbidden, unauthorized, isNotFound, isRedirect, isForbidden, isUnauthorized } from './src/nav.js';
17
17
  export { cspNonce, setCspNonceProvider } from './src/csp-nonce.js';
18
18
  export { asset, setAssetUrlProvider } from './src/asset-url.js';
19
+ // Form actions (#1155). `FORM_ACTION_FIELD` is the hidden field both renderers
20
+ // emit and the server's form dispatcher reads; `setFormActionResolver` is the
21
+ // server-only wiring that maps a real action function back to its identity.
22
+ export { FORM_ACTION_FIELD, FORM_ACTION_ID_KEY, setFormActionResolver } from './src/form-action.js';
19
23
  export { repeat, isRepeat } from './src/repeat.js';
20
24
  export { Suspense, isSuspense } from './src/suspense.js';
21
25
  export { connectWS } from './src/websocket-client.js';
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@webjsdev/core",
3
- "version": "0.7.46",
3
+ "version": "0.7.47",
4
4
  "type": "module",
5
- "description": "webjs core runtime - html/css tags, WebComponent base, isomorphic renderers",
5
+ "description": "The runtime for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Ships the html and css template tags, the WebComponent base class, signals, directives, and the isomorphic renderers.",
6
6
  "types": "./index.d.ts",
7
7
  "exports": {
8
8
  ".": {
package/src/component.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { render as clientRender } from './render-client.js';
2
2
  import { setActiveActionSignal } from './action-abort-client.js';
3
+ import { carriesFunction } from './form-action.js';
3
4
  import { isCSS, adoptStyles } from './css.js';
4
5
  import { register, tagOf } from './registry.js';
5
6
  import { parse as deserializeProp } from './serialize.js';
@@ -118,6 +119,44 @@ function safeString(v) {
118
119
  }
119
120
  }
120
121
 
122
+ /**
123
+ * Warn that a function value was dropped rather than reflected (#1169).
124
+ *
125
+ * A silently missing attribute is its own confusion, so say what went and why.
126
+ * The message deliberately does NOT include the value: printing the source is
127
+ * the leak this guard exists to prevent, and a server log is not always a
128
+ * private place.
129
+ *
130
+ * UNCONDITIONAL, matching the `.prop=${fn}` unserializable-value drop in
131
+ * `render-server.js`, which is the sibling path this guard mirrors. Two
132
+ * reasons not to gate it on a dev flag. It fires only on a genuine mistake (a
133
+ * function is never a meaningful attribute value), so there is no volume to
134
+ * suppress, and reflection runs per assignment rather than per frame.
135
+ *
136
+ * More decisively, a `NODE_ENV` gate does not survive the dist build.
137
+ * `scripts/build-framework-dist.js` runs esbuild with `platform: 'browser'`
138
+ * and `minify: true`, which substitutes `process.env.NODE_ENV` with
139
+ * `"production"` and folds the check to a constant. The SSR half of the
140
+ * warning would then be unreachable in every published build, which is how
141
+ * every installed app runs, and SSR is the half that matters most, since that
142
+ * is where the leaked source reached visitors.
143
+ *
144
+ * @param {{ constructor: unknown, tagName?: string }} host
145
+ * @param {string} propName
146
+ * @param {string} attrName
147
+ */
148
+ function warnFunctionReflection(host, propName, attrName) {
149
+ if (typeof console === 'undefined' || !console.warn) return;
150
+ const tag = tagOf(/** @type any */ (host.constructor)) || host.tagName?.toLowerCase() || 'unknown';
151
+ console.warn(
152
+ `[webjs] reflect:true property "${propName}" on <${tag}> holds a function `
153
+ + `(or an array carrying one), which has no HTML attribute representation. `
154
+ + `Removing "${attrName}" instead of stringifying it (a stringified function `
155
+ + `writes its source into the page, so a server action's body would ship to `
156
+ + `the browser). Pass a string, or drop reflect on this property.`
157
+ );
158
+ }
159
+
121
160
  /**
122
161
  * A minimal base for HTML Custom Elements that mirrors Lit's ergonomics
123
162
  * while staying JSDoc-only and no-build.
@@ -685,13 +724,43 @@ class WebComponentBase extends Base {
685
724
  const serialized = decl.converter.toAttribute(value, decl.type);
686
725
  if (serialized == null) this.removeAttribute(attrName);
687
726
  else this.setAttribute(attrName, serialized);
727
+ } else if (typeof value === 'function') {
728
+ // #1169: the value IS a function, and no branch below has a
729
+ // meaningful serialization for one. `String(fn)` is the function's
730
+ // SOURCE, so a reflected `'use server'` action would ship its whole
731
+ // body, closure secrets included, to every visitor, and
732
+ // `JSON.stringify(fn)` is `undefined`, which `setAttribute` writes as
733
+ // the literal string "undefined". Treat it like `null` and remove the
734
+ // attribute, matching what the `.prop=${fn}` SSR binding already does
735
+ // for an unserializable value.
736
+ //
737
+ // Placed AFTER the converter branch on purpose: a custom
738
+ // `toAttribute` is author-controlled, so its author has taken
739
+ // responsibility for serializing whatever they are handed.
740
+ this.removeAttribute(attrName);
741
+ warnFunctionReflection(this, propName, attrName);
688
742
  } else if (decl.type === Boolean) {
689
743
  if (value) this.setAttribute(attrName, '');
690
744
  else this.removeAttribute(attrName);
691
745
  } else if (value == null) {
692
746
  this.removeAttribute(attrName);
693
747
  } else if (decl.type === Object || decl.type === Array) {
748
+ // A JSON-typed prop CARRYING a function is safe and stays whole:
749
+ // `JSON.stringify` drops a function to `null` in an array and omits
750
+ // the key in an object, so `[1, 2, fn]` serializes to `[1,2,null]`
751
+ // with no source and no data loss. Refusing here would discard the
752
+ // `1` and the `2` on a path that never leaked.
694
753
  this.setAttribute(attrName, JSON.stringify(value));
754
+ } else if (carriesFunction(value)) {
755
+ // The string fall-through is the one place a CARRIED function still
756
+ // leaks. `String([fn])` is `Array.prototype.join`, which runs
757
+ // `String()` on every element, so `[serverAction]` writes the same
758
+ // source `serverAction` does. Hence the recursive predicate the
759
+ // form-action guard already uses, rather than a bare `typeof`, but
760
+ // scoped to this branch rather than applied above, where JSON
761
+ // handles the same shape losslessly.
762
+ this.removeAttribute(attrName);
763
+ warnFunctionReflection(this, propName, attrName);
695
764
  } else {
696
765
  this.setAttribute(attrName, String(value));
697
766
  }
@@ -0,0 +1,16 @@
1
+ /** Form actions (#1155): `<form action=${importedAction}>` and its wire. */
2
+
3
+ /** The hidden field carrying a bound action's identity into the submission. */
4
+ export const FORM_ACTION_FIELD: '__webjs_action';
5
+
6
+ /** The property a generated client RPC stub carries its own identity on. */
7
+ export const FORM_ACTION_ID_KEY: '$$webjsAction';
8
+
9
+ /**
10
+ * Server-only wiring: maps a REAL server-action function back to its
11
+ * `<hash>/<fn>` identity. `@webjsdev/server` installs it at boot; the browser
12
+ * bundle drops it, because a stub carries its identity on itself.
13
+ */
14
+ export function setFormActionResolver(
15
+ fn: (fn: Function) => string | null | Promise<string | null>,
16
+ ): void;