@webjsdev/core 0.8.0 → 0.9.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 (81) hide show
  1. package/README.md +5 -0
  2. package/dist/webjs-core-browser.js +6 -5
  3. package/dist/webjs-core-browser.js.map +3 -3
  4. package/dist/webjs-core-testing.js +20 -19
  5. package/dist/webjs-core-testing.js.map +3 -3
  6. package/dist/webjs-core.js +7 -6
  7. package/dist/webjs-core.js.map +3 -3
  8. package/index-browser.js +14 -9
  9. package/index.d.ts +22 -10
  10. package/index.js +17 -9
  11. package/package.json +15 -2
  12. package/src/action-effects-client.d.ts +5 -0
  13. package/src/action-effects-client.js +84 -0
  14. package/src/action-seed-client.js +5 -5
  15. package/src/asset-url.js +12 -13
  16. package/src/async-directive.d.ts +18 -0
  17. package/src/async-directive.js +88 -0
  18. package/src/attribute-reader.js +197 -23
  19. package/src/binding-prefixes.js +0 -12
  20. package/src/component/lifecycle.js +760 -209
  21. package/src/component/reactive.js +2 -10
  22. package/src/component/server-element.js +23 -1
  23. package/src/component.d.ts +151 -41
  24. package/src/context.d.ts +94 -18
  25. package/src/context.js +341 -217
  26. package/src/converters.d.ts +7 -0
  27. package/src/converters.js +38 -0
  28. package/src/css.d.ts +32 -6
  29. package/src/css.js +178 -14
  30. package/src/directive.d.ts +58 -0
  31. package/src/directive.js +119 -0
  32. package/src/directives.d.ts +38 -1
  33. package/src/directives.js +158 -1
  34. package/src/form-action.d.ts +13 -0
  35. package/src/form-action.js +139 -4
  36. package/src/hot.js +23 -0
  37. package/src/html.d.ts +4 -0
  38. package/src/html.js +35 -3
  39. package/src/is-server.d.ts +2 -0
  40. package/src/is-server.js +18 -0
  41. package/src/jsx-guard.js +130 -0
  42. package/src/metadata.d.ts +22 -3
  43. package/src/nav.d.ts +4 -1
  44. package/src/nav.js +44 -11
  45. package/src/optimistic.js +2 -2
  46. package/src/refresh-header.js +52 -0
  47. package/src/registry.js +10 -1
  48. package/src/render-client/adopt-slots.js +8 -3
  49. package/src/render-client/parts.js +745 -31
  50. package/src/render-client/reconciler.js +56 -11
  51. package/src/render-client/template-compiler.js +139 -44
  52. package/src/render-client.d.ts +5 -1
  53. package/src/render-server/attrs.js +2 -2
  54. package/src/render-server/compile.js +34 -0
  55. package/src/render-server/errors.js +37 -0
  56. package/src/render-server/expand.js +61 -8
  57. package/src/render-server/served-attrs.js +1 -2
  58. package/src/render-server/walk.js +135 -3
  59. package/src/render-server.d.ts +3 -0
  60. package/src/render-server.js +2 -0
  61. package/src/repeat.d.ts +7 -2
  62. package/src/repeat.js +20 -6
  63. package/src/router-client/dom-differ.js +17 -0
  64. package/src/router-client/fetch-apply.js +86 -5
  65. package/src/router-client/navigator.js +45 -9
  66. package/src/router-client/prefetch.js +3 -4
  67. package/src/router-client/root-attrs.js +43 -0
  68. package/src/router-client/stream.js +7 -0
  69. package/src/router-client/swap.js +4 -1
  70. package/src/routes.d.ts +75 -18
  71. package/src/serialize.d.ts +2 -0
  72. package/src/serialize.js +35 -4
  73. package/src/signal.d.ts +7 -0
  74. package/src/signal.js +26 -6
  75. package/src/slot/polyfills.js +3 -3
  76. package/src/slot.js +4 -8
  77. package/src/static-html.d.ts +22 -0
  78. package/src/static-html.js +144 -0
  79. package/src/task.d.ts +63 -20
  80. package/src/task.js +326 -293
  81. package/src/webjs-config.d.ts +1 -8
package/index-browser.js CHANGED
@@ -23,25 +23,27 @@
23
23
  * Keep this list in sync with `./index.js` for everything else.
24
24
  */
25
25
 
26
- export { html, svg, isTemplate, MARKER } from './src/html.js';
27
- export { css, isCSS, adoptStyles, stylesToString } from './src/css.js';
26
+ export { html, svg, mathml, isTemplate, MARKER } from './src/html.js';
27
+ export { isServer } from './src/is-server.js';
28
+ export { css, unsafeCSS, CSSResult, isCSS, adoptStyles, stylesToString } from './src/css.js';
28
29
  export { WebComponent, prop } from './src/component.js';
30
+ export { defaultConverter, notEqual } from './src/converters.js';
29
31
  export { lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
30
32
  export { render } from './src/render-client.js';
31
33
  export { escapeText, escapeAttr } from './src/escape.js';
32
- export { notFound, redirect, forbidden, unauthorized, isNotFound, isRedirect, isForbidden, isUnauthorized } from './src/nav.js';
34
+ export { notFound, redirect, permanentRedirect, forbidden, unauthorized, isNotFound, isRedirect, isForbidden, isUnauthorized } from './src/nav.js';
33
35
  export { cspNonce } from './src/csp-nonce.js';
34
36
  export { asset } from './src/asset-url.js';
35
37
  // Form actions (#1155). The field name is shared with the server dispatcher;
36
38
  // `setFormActionResolver` is server-only wiring and is stripped here, because
37
39
  // a browser stub carries its own identity and needs no resolver.
38
- export { FORM_ACTION_FIELD, FORM_ACTION_ID_KEY } from './src/form-action.js';
40
+ export { FORM_ACTION_FIELD, FORM_ACTION_ID_KEY, installActionBind, bindActionId, splitActionId } from './src/form-action.js';
39
41
  export { repeat, isRepeat } from './src/repeat.js';
40
42
  export { Suspense, isSuspense } from './src/suspense.js';
41
43
  export { connectWS } from './src/websocket-client.js';
42
44
  export { richFetch } from './src/rich-fetch.js';
43
45
  export {
44
- stringify, parse,
46
+ stringify, stringifySync, parse,
45
47
  serialize, deserialize,
46
48
  } from './src/serialize.js';
47
49
  // The client router auto-enables on bundle load: `router-client.js` calls
@@ -65,7 +67,7 @@ export { WebjsFrame } from './src/webjs-frame.js';
65
67
  export { WebjsStream, renderStream } from './src/webjs-stream.js';
66
68
 
67
69
  // Signals (TC39 Stage-1 shape), also available via '@webjsdev/core/signals'
68
- export { signal, computed, effect, batch, isSignal, Signal } from './src/signal.js';
70
+ export { signal, computed, effect, batch, isSignal, Signal, SignalWatcher } from './src/signal.js';
69
71
 
70
72
  // Optimistic-mutation helper (thin signal wrapper, rolls back on failure)
71
73
  export { optimistic } from './src/optimistic.js';
@@ -76,6 +78,9 @@ export { takeSeed, scanSeeds, seedStats, SEED_MISS } from './src/action-seed-cli
76
78
  // Client tag-cache coordinator for HTTP-verb actions (#488): tag-based
77
79
  // browser-cache eviction after a mutation. Inert server-side.
78
80
  export { markStale, registerKeyTags, consumeStale, parseTagHeader, fetchMark } from './src/action-cache-client.js';
81
+ // Effects an RPC action response asks the browser for (revalidatePath's
82
+ // in-place refresh), read by the generated action stub.
83
+ export { applyActionRefresh, followActionRedirect } from './src/action-effects-client.js';
79
84
  // Client action-abort plumbing (#492): a superseded async render aborts its
80
85
  // in-flight action fetches. Inert server-side.
81
86
  export { setActiveActionSignal, activeActionSignal } from './src/action-abort-client.js';
@@ -92,14 +97,14 @@ export {
92
97
  unsafeHTML, isUnsafeHTML, live, isLive, keyed, isKeyed, guard, isGuard,
93
98
  templateContent, isTemplateContent, ref, isRef, createRef, cache, isCache,
94
99
  until, isUntil, asyncAppend, isAsyncAppend, asyncReplace, isAsyncReplace,
95
- watch, isWatch,
100
+ watch, isWatch, unsafeSVG, unsafeMathML, when, choose, map, join, range,
96
101
  } from './src/directives.js';
97
102
 
98
103
  // Context Protocol, also available via '@webjsdev/core/context'
99
- export { createContext, ContextProvider, ContextConsumer, ContextRequestEvent } from './src/context.js';
104
+ export { createContext, ContextProvider, ContextConsumer, ContextRequestEvent, ContextProviderEvent, ContextRoot } from './src/context.js';
100
105
 
101
106
  // Task controller, also available via '@webjsdev/core/task'
102
- export { Task, TaskStatus } from './src/task.js';
107
+ export { Task, TaskStatus, initialState, shallowArrayEquals, deepArrayEquals } from './src/task.js';
103
108
 
104
109
  export { nothing, noChange } from './src/sentinels.js';
105
110
 
package/index.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  export * from './src/component.js';
12
+ export * from './src/converters.js';
12
13
  export type {
13
14
  Metadata,
14
15
  MetadataContext,
@@ -25,6 +26,7 @@ export type {
25
26
  AuthorMetadata,
26
27
  PreloadDescriptor,
27
28
  JsonLd,
29
+ GenerateMetadata,
28
30
  } from './src/metadata.d.ts';
29
31
 
30
32
  // Typed route props + the opt-in generated route union (#258). The
@@ -36,13 +38,14 @@ export type {
36
38
  RouteParams,
37
39
  PageProps,
38
40
  LayoutProps,
41
+ FormActionData,
39
42
  RouteHandlerContext,
40
43
  WebjsRoutes,
41
44
  RouteParamMap,
42
45
  } from './src/routes.d.ts';
43
46
 
44
47
  // The package.json `webjs` config block (#259). Typed reference for the
45
- // elide / headers / redirects / trailingSlash / csp / ingress-limit knobs;
48
+ // seed / headers / redirects / trailingSlash / csp / ingress-limit knobs;
46
49
  // the companion JSON Schema (packages/server/webjs-config.schema.json) gives
47
50
  // editors native validation of package.json itself.
48
51
  export type {
@@ -74,20 +77,25 @@ export type {
74
77
  NonSerializable,
75
78
  } from './src/serializable.d.ts';
76
79
 
77
- export { html, svg, isTemplate, MARKER } from './src/html.js';
80
+ export { html, svg, mathml, isTemplate, MARKER } from './src/html.js';
81
+ export { isServer } from './src/is-server.js';
78
82
  // The `html\`\`` return type. A JSDoc typedef in html.js used across the
79
83
  // component / route / testing type surfaces; re-exported so an app or the
80
84
  // docs/website can annotate a value as `TemplateResult` (#772).
81
85
  export type { TemplateResult } from './src/html.js';
82
- export { css, isCSS, adoptStyles, stylesToString } from './src/css.js';
86
+ export { css, unsafeCSS, CSSResult, isCSS, adoptStyles, stylesToString } from './src/css.js';
87
+ export type { CSSResultGroup, CSSResultOrNative, CSSResultArray } from './src/css.js';
83
88
  export { lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
84
89
  export { renderToString, renderToStream } from './src/render-server.js';
85
90
  export { render } from './src/render-client.js';
91
+ export type { RootPart } from './src/render-client.js';
86
92
  export { escapeText, escapeAttr } from './src/escape.js';
87
- export { notFound, redirect, forbidden, unauthorized, isNotFound, isRedirect, isForbidden, isUnauthorized } from './src/nav.js';
93
+ export { notFound, redirect, permanentRedirect, forbidden, unauthorized, isNotFound, isRedirect, isForbidden, isUnauthorized } from './src/nav.js';
94
+ export type { RedirectType } from './src/nav.js';
88
95
  export { repeat, isRepeat } from './src/repeat.js';
89
96
  export { Suspense, isSuspense } from './src/suspense.js';
90
97
  export { connectWS } from './src/websocket-client.js';
98
+ export type { WSConnection, ConnectOptions } from './src/websocket-client.js';
91
99
  export { richFetch } from './src/rich-fetch.js';
92
100
  export { enableClientRouter, disableClientRouter, revalidate, refreshPage, loadFrame } from './src/router-client.js';
93
101
 
@@ -103,21 +111,25 @@ export {
103
111
  unsafeHTML, isUnsafeHTML, live, isLive, keyed, isKeyed, guard, isGuard,
104
112
  templateContent, isTemplateContent, ref, isRef, createRef, cache, isCache,
105
113
  until, isUntil, asyncAppend, isAsyncAppend, asyncReplace, isAsyncReplace,
106
- watch, isWatch,
114
+ watch, isWatch, unsafeSVG, unsafeMathML, when, choose, map, join, range,
107
115
  } from './src/directives.js';
108
- export { createContext, ContextProvider, ContextConsumer, ContextRequestEvent } from './src/context.js';
109
- export { Task, TaskStatus } from './src/task.js';
116
+ export { createContext, ContextProvider, ContextConsumer, ContextRequestEvent, ContextProviderEvent, ContextRoot } from './src/context.js';
117
+ export { Task, TaskStatus, initialState, shallowArrayEquals, deepArrayEquals } from './src/task.js';
110
118
 
111
119
  // Signals (the default state primitive, invariant 5), the CSP nonce reader,
112
120
  // the wire serializer, and the streaming/frame custom elements. These mirror
113
121
  // the index.js runtime re-exports; their declarations live in the matching
114
122
  // src/*.d.ts (#388 fixed the index.d.ts drift from index.js).
115
- export { signal, computed, effect, batch, isSignal, Signal } from './src/signal.js';
123
+ export { signal, computed, effect, batch, isSignal, Signal, SignalWatcher } from './src/signal.js';
116
124
  export { cspNonce, setCspNonceProvider } from './src/csp-nonce.js';
117
125
  export { asset, setAssetUrlProvider } from './src/asset-url.js';
118
- export { FORM_ACTION_FIELD, FORM_ACTION_ID_KEY, setFormActionResolver } from './src/form-action.js';
126
+ export {
127
+ FORM_ACTION_FIELD, FORM_ACTION_ID_KEY, setFormActionResolver,
128
+ installActionBind, bindActionId, splitActionId,
129
+ } from './src/form-action.js';
119
130
  export { takeSeed, scanSeeds, seedStats, SEED_MISS } from './src/action-seed-client.js';
120
131
  export { markStale, registerKeyTags, consumeStale, parseTagHeader, fetchMark } from './src/action-cache-client.js';
132
+ export { applyActionRefresh, followActionRedirect } from './src/action-effects-client.js';
121
133
  // Client action-abort plumbing (#492): a superseded async render aborts its
122
134
  // in-flight action fetches. Inert server-side.
123
135
  export { setActiveActionSignal, activeActionSignal } from './src/action-abort-client.js';
@@ -125,7 +137,7 @@ export { setActiveActionSignal, activeActionSignal } from './src/action-abort-cl
125
137
  export {
126
138
  STREAM_CONTENT_TYPE, FRAME_CHUNK, FRAME_END, FRAME_ERROR, encodeFrame, createFrameDecoder,
127
139
  } from './src/action-stream.js';
128
- export { stringify, parse, serialize, deserialize } from './src/serialize.js';
140
+ export { stringify, stringifySync, parse, serialize, deserialize } from './src/serialize.js';
129
141
  export { WebjsFrame } from './src/webjs-frame.js';
130
142
  export { WebjsStream, renderStream } from './src/webjs-stream.js';
131
143
 
package/index.js CHANGED
@@ -6,26 +6,31 @@
6
6
  * renderer is only reached on the server.
7
7
  */
8
8
 
9
- export { html, svg, isTemplate, MARKER } from './src/html.js';
10
- export { css, isCSS, adoptStyles, stylesToString } from './src/css.js';
9
+ export { html, svg, mathml, isTemplate, MARKER } from './src/html.js';
10
+ export { isServer } from './src/is-server.js';
11
+ export { css, unsafeCSS, CSSResult, isCSS, adoptStyles, stylesToString } from './src/css.js';
11
12
  export { WebComponent, prop } from './src/component.js';
13
+ export { defaultConverter, notEqual } from './src/converters.js';
12
14
  export { lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
13
15
  export { renderToString, renderToStream } from './src/render-server.js';
14
16
  export { render } from './src/render-client.js';
15
17
  export { escapeText, escapeAttr } from './src/escape.js';
16
- export { notFound, redirect, forbidden, unauthorized, isNotFound, isRedirect, isForbidden, isUnauthorized } from './src/nav.js';
18
+ export { notFound, redirect, permanentRedirect, forbidden, unauthorized, isNotFound, isRedirect, isForbidden, isUnauthorized } from './src/nav.js';
17
19
  export { cspNonce, setCspNonceProvider } from './src/csp-nonce.js';
18
20
  export { asset, setAssetUrlProvider } from './src/asset-url.js';
19
21
  // Form actions (#1155). `FORM_ACTION_FIELD` is the hidden field both renderers
20
22
  // emit and the server's form dispatcher reads; `setFormActionResolver` is the
21
23
  // 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';
24
+ export {
25
+ FORM_ACTION_FIELD, FORM_ACTION_ID_KEY, setFormActionResolver,
26
+ installActionBind, bindActionId, splitActionId,
27
+ } from './src/form-action.js';
23
28
  export { repeat, isRepeat } from './src/repeat.js';
24
29
  export { Suspense, isSuspense } from './src/suspense.js';
25
30
  export { connectWS } from './src/websocket-client.js';
26
31
  export { richFetch } from './src/rich-fetch.js';
27
32
  export {
28
- stringify, parse,
33
+ stringify, stringifySync, parse,
29
34
  serialize, deserialize,
30
35
  } from './src/serialize.js';
31
36
  export { enableClientRouter, disableClientRouter, navigate, revalidate, refreshPage, loadFrame } from './src/router-client.js';
@@ -33,7 +38,7 @@ export { WebjsFrame } from './src/webjs-frame.js';
33
38
  export { WebjsStream, renderStream } from './src/webjs-stream.js';
34
39
 
35
40
  // Signals (TC39 Stage-1 shape), also available via '@webjsdev/core/signals'
36
- export { signal, computed, effect, batch, isSignal, Signal } from './src/signal.js';
41
+ export { signal, computed, effect, batch, isSignal, Signal, SignalWatcher } from './src/signal.js';
37
42
 
38
43
  // Optimistic-mutation helper (thin signal wrapper, rolls back on failure)
39
44
  export { optimistic } from './src/optimistic.js';
@@ -44,6 +49,9 @@ export { takeSeed, scanSeeds, seedStats, SEED_MISS } from './src/action-seed-cli
44
49
  // Client tag-cache coordinator for HTTP-verb actions (#488): tag-based
45
50
  // browser-cache eviction after a mutation. Inert server-side.
46
51
  export { markStale, registerKeyTags, consumeStale, parseTagHeader, fetchMark } from './src/action-cache-client.js';
52
+ // Effects an RPC action response asks the browser for (revalidatePath's
53
+ // in-place refresh), read by the generated action stub.
54
+ export { applyActionRefresh, followActionRedirect } from './src/action-effects-client.js';
47
55
  // Client action-abort plumbing (#492): a superseded async render aborts its
48
56
  // in-flight action fetches. Inert server-side.
49
57
  export { setActiveActionSignal, activeActionSignal } from './src/action-abort-client.js';
@@ -60,14 +68,14 @@ export {
60
68
  unsafeHTML, isUnsafeHTML, live, isLive, keyed, isKeyed, guard, isGuard,
61
69
  templateContent, isTemplateContent, ref, isRef, createRef, cache, isCache,
62
70
  until, isUntil, asyncAppend, isAsyncAppend, asyncReplace, isAsyncReplace,
63
- watch, isWatch,
71
+ watch, isWatch, unsafeSVG, unsafeMathML, when, choose, map, join, range,
64
72
  } from './src/directives.js';
65
73
 
66
74
  // Context Protocol, also available via '@webjsdev/core/context'
67
- export { createContext, ContextProvider, ContextConsumer, ContextRequestEvent } from './src/context.js';
75
+ export { createContext, ContextProvider, ContextConsumer, ContextRequestEvent, ContextProviderEvent, ContextRoot } from './src/context.js';
68
76
 
69
77
  // Task controller, also available via '@webjsdev/core/task'
70
- export { Task, TaskStatus } from './src/task.js';
78
+ export { Task, TaskStatus, initialState, shallowArrayEquals, deepArrayEquals } from './src/task.js';
71
79
 
72
80
  export { nothing, noChange } from './src/sentinels.js';
73
81
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/core",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
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",
@@ -58,7 +58,20 @@
58
58
  },
59
59
  "./signals": {
60
60
  "types": "./src/signal.d.ts",
61
- "default": "./src/signal.js"
61
+ "source": "./src/signal.js",
62
+ "default": "./dist/webjs-core-browser.js"
63
+ },
64
+ "./directive": {
65
+ "types": "./src/directive.d.ts",
66
+ "default": "./src/directive.js"
67
+ },
68
+ "./async-directive": {
69
+ "types": "./src/async-directive.d.ts",
70
+ "default": "./src/async-directive.js"
71
+ },
72
+ "./static-html": {
73
+ "types": "./src/static-html.d.ts",
74
+ "default": "./src/static-html.js"
62
75
  },
63
76
  "./package.json": "./package.json"
64
77
  },
@@ -0,0 +1,5 @@
1
+ /** Browser effects a server action's RPC response asks for (revalidatePath's in-place refresh). Read by the generated action stub. */
2
+ export function parseRefreshHeader(value: string | null | undefined): { path: string; layout: boolean }[];
3
+ export function refreshCovers(entry: { path: string; layout: boolean }, pathname: string): boolean;
4
+ export function applyActionRefresh(value: string | null | undefined): Promise<boolean>;
5
+ export function followActionRedirect(value: string): Promise<never>;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Browser-side effects a server action's RPC response asks for, the Next.js
3
+ * behaviours an `await action()` call has after the action ran:
4
+ *
5
+ * - `x-webjs-refresh` (written when the action called `revalidatePath`):
6
+ * evict the client router's cached copies of those paths and, when the
7
+ * page on screen is one of them, re-render it in place (`refreshPage`), so
8
+ * the caller sees fresh server data with no reload.
9
+ * - `x-webjs-redirect` (the action threw `redirect()` / `permanentRedirect()`):
10
+ * navigate there with the client router (a full load for another origin),
11
+ * and reject the call with the redirect sentinel, so code after
12
+ * `await action()` does not run, exactly as in Next.
13
+ *
14
+ * The generated action stub calls these; app code does not need to.
15
+ *
16
+ * @module action-effects-client
17
+ */
18
+ import { revalidate, refreshPage, navigate } from './router-client/navigator.js';
19
+ import { isRedirect } from './nav.js';
20
+ import { parseRefreshHeader, refreshCovers } from './refresh-header.js';
21
+
22
+ export { parseRefreshHeader, refreshCovers };
23
+
24
+ /**
25
+ * Apply an `x-webjs-refresh` header: evict the cached snapshots it names, and
26
+ * refresh the current page in place when it is one of them (a `layout` entry
27
+ * refreshes the whole shell, since the layout's own markup may have changed).
28
+ * Resolves once the refresh applied, so `await action()` returns after the
29
+ * page shows the new data, as in Next.
30
+ * @param {string | null | undefined} value
31
+ * @returns {Promise<boolean>} whether the current page was refreshed
32
+ */
33
+ export async function applyActionRefresh(value) {
34
+ const entries = parseRefreshHeader(value);
35
+ if (!entries.length || typeof location === 'undefined') return false;
36
+ let current = false;
37
+ let shell = false;
38
+ for (const e of entries) {
39
+ const pattern = e.layout || e.path.includes('[');
40
+ if (pattern) revalidate();
41
+ else revalidate(e.path);
42
+ if (refreshCovers(e, location.pathname)) {
43
+ current = true;
44
+ if (e.layout) shell = true;
45
+ }
46
+ }
47
+ if (!current) return false;
48
+ try { return await refreshPage(shell ? 'shell' : 'page'); } catch { return false; }
49
+ }
50
+
51
+ let rejectionFilterInstalled = false;
52
+
53
+ /**
54
+ * Follow an `x-webjs-redirect` header (`<url>;<push|replace>`): a same-origin
55
+ * target goes through the client router, another origin is a full load.
56
+ * Returns a promise rejected with the redirect sentinel, and makes sure an
57
+ * unhandled one (a fire-and-forget `action()` in a click handler) is not
58
+ * reported as an error, since it is navigation.
59
+ * @param {string} value
60
+ * @returns {Promise<never>}
61
+ */
62
+ export function followActionRedirect(value) {
63
+ const cut = value.lastIndexOf(';');
64
+ const url = cut === -1 ? value : value.slice(0, cut);
65
+ const replace = cut !== -1 && value.slice(cut + 1) === 'replace';
66
+ const err = new Error(`webjs: redirect(${url})`);
67
+ /** @type {any} */ (err).__webjs = Symbol.for('webjs.redirect');
68
+ /** @type {any} */ (err).url = url;
69
+ if (typeof location !== 'undefined') {
70
+ if (!rejectionFilterInstalled && typeof addEventListener === 'function') {
71
+ rejectionFilterInstalled = true;
72
+ addEventListener('unhandledrejection', (e) => { if (isRedirect(e.reason)) e.preventDefault(); });
73
+ }
74
+ let target;
75
+ try { target = new URL(url, location.href); } catch { target = null; }
76
+ if (target && target.origin === location.origin) {
77
+ Promise.resolve(navigate(target.pathname + target.search + target.hash, { replace })).catch(() => location.assign(target.href));
78
+ } else if (target) {
79
+ if (replace) location.replace(target.href);
80
+ else location.assign(target.href);
81
+ }
82
+ }
83
+ return Promise.reject(err);
84
+ }
@@ -165,7 +165,7 @@ export function scanSeeds(root, opts) {
165
165
 
166
166
  // The live document can still be holding the OUTGOING page's block, because
167
167
  // the initial scan is lazy (it runs on the first `takeSeed`) and a page whose
168
- // async components all elided never triggers it, while the block sits after
168
+ // async components are all server-only never triggers it, while the block sits after
169
169
  // the body content outside every boundary range, so no swap removes it either.
170
170
  //
171
171
  // Strip it WITHOUT ingesting. Those values belong to a page that is being
@@ -173,7 +173,7 @@ export function scanSeeds(root, opts) {
173
173
  // them for the lazy scan to ingest LATER is what let a departed page's value
174
174
  // win a key under last-write-wins and contradict the paint. Ingesting them
175
175
  // HERE would order them correctly but keep them forever, because the very
176
- // reason the block went unconsumed is that its components elided, so nothing
176
+ // reason the block went unconsumed is that its components never ship, so nothing
177
177
  // will ever call `takeSeed` for those keys and nothing but a hit deletes one.
178
178
  // That would grow the store by a whole page payload per navigation, in
179
179
  // production, and inflate the "still unconsumed" figure in the dev line with
@@ -183,9 +183,9 @@ export function scanSeeds(root, opts) {
183
183
  if (incomingPage) {
184
184
  drainCarriers(live, true);
185
185
  // And evict what the outgoing page ALREADY ingested. Stripping the DOM
186
- // carrier is not enough: a page carrying an elided component alongside a
186
+ // carrier is not enough: a page carrying a server-only component alongside a
187
187
  // shipping one has its WHOLE block ingested by the lazy scan the shipping
188
- // one triggers, so the elided component's keys are in the store with no
188
+ // one triggers, so the server-only component's keys are in the store with no
189
189
  // carrier left to strip and nothing that will ever consume them. Navigate to
190
190
  // a page that emits no seed for such a key (a streamed page emits no block
191
191
  // at all) and a component calling the same action with the same arguments
@@ -424,7 +424,7 @@ export function takeSeed(hash, fnName, argsKey) {
424
424
  * `replaced` are what `scanSeeds` merged; `hits` and `misses` are what the
425
425
  * generated RPC stubs asked for; `pending` is what is still in the store
426
426
  * unconsumed (a non-zero `pending` at rest usually means the seeding component
427
- * elided, so nothing on the client was ever going to call it).
427
+ * is server-only, so nothing on the client was ever going to call it).
428
428
  * `keyMisses` is the provable subset of `misses`: a call for an action this page
429
429
  * DID seed under other arguments, which is the only miss the client can call a
430
430
  * defect (see `seedFns`).
package/src/asset-url.js CHANGED
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Resolve a `public/` asset url to its content-fingerprinted form, so a
2
+ * Resolve a `public/` asset url (served at the url ROOT: `public/app.css` is
3
+ * `/app.css`, as in Next.js) to its content-fingerprinted form, so a
3
4
  * deploy that changes the file changes the url and no cache can serve the
4
5
  * previous bytes.
5
6
  *
6
7
  * import { html, asset } from '@webjsdev/core';
7
- * html`<link rel="stylesheet" href=${asset('/public/app.css')}>`
8
+ * html`<link rel="stylesheet" href=${asset('/app.css')}>`
8
9
  *
9
10
  * On the server, `@webjsdev/server` installs a provider that appends
10
11
  * `?v=<content-hash>`; the framework then serves a `?v=`-carrying request
@@ -18,7 +19,7 @@
18
19
  * the hydrate marker and `createInstance` replaces the SSR'd children), so
19
20
  * the client value overwrites the server one. With no provider in the
20
21
  * browser, `asset()` returns the bare path there, so an `<img src=${asset(
21
- * '/public/logo.svg')}>` inside a shipping component paints the hashed url
22
+ * '/logo.svg')}>` inside a shipping component paints the hashed url
22
23
  * at SSR and then swaps it for the un-hashed one on upgrade, fetching the
23
24
  * same bytes twice and leaving the short-lived url in the DOM. It still
24
25
  * WORKS (the path is always valid), which is why this is a convention rather
@@ -28,13 +29,11 @@
28
29
  * The sibling `cspNonce()` carries the same server/browser asymmetry and the
29
30
  * same scoping, for the same hydration reason.
30
31
  *
31
- * Call it INSIDE the render function, not at module scope. A depth-0 call is a
32
- * module-scope side effect, which the elision analyser reads as client work, so
33
- * hoisting `const CSS = asset('/public/app.css')` pins the whole page or layout
34
- * into the browser bundle for no benefit. Resolver ordering is not the issue
35
- * (the server installs its resolver at boot, before any app module loads).
32
+ * Calling it at module scope works too: the server installs its resolver at
33
+ * boot, before any app module loads, so a hoisted `const CSS =
34
+ * asset('/app.css')` in a page or layout resolves the same as one in render.
36
35
  *
37
- * Under `webjs.basePath`, write the prefix yourself: `asset('/app/public/x.css')`.
36
+ * Under `webjs.basePath`, write the prefix yourself: `asset('/app/x.css')`.
38
37
  * The framework only base-path-prefixes urls IT emits, so an author-written
39
38
  * url is already your responsibility (an un-prefixed one 404s under a
40
39
  * sub-path deploy with or without this helper). `asset()` strips the prefix
@@ -58,9 +57,9 @@
58
57
  * exactly the set of urls someone deliberately marked and nothing else in the
59
58
  * document is ever read or rewritten.
60
59
  *
61
- * Scope note: only a `public/` path is fingerprinted, matching what the
62
- * static-asset route will actually serve. Anything else is returned
63
- * unchanged rather than guessed at.
60
+ * Scope note: only a file that exists under `public/` is fingerprinted,
61
+ * matching what the static-asset route will actually serve. Anything else is
62
+ * returned unchanged rather than guessed at.
64
63
  */
65
64
 
66
65
  /** @type {((path: string) => string) | null} */
@@ -85,7 +84,7 @@ export function setAssetUrlProvider(fn) {
85
84
  * resolved. Every failure mode degrades to the plain path, never to a broken
86
85
  * one.
87
86
  *
88
- * @param {string} path a root-absolute same-origin path, e.g. `/public/app.css`
87
+ * @param {string} path a root-absolute same-origin path, e.g. `/app.css`
89
88
  * @returns {string}
90
89
  */
91
90
  export function asset(path) {
@@ -0,0 +1,18 @@
1
+ import { Directive } from './directive.js';
2
+ import type { PartInfo } from './directive.js';
3
+
4
+ export { directive, Directive, PartType } from './directive.js';
5
+ export type { PartInfo, Part, DirectiveResult, DirectiveParameters } from './directive.js';
6
+
7
+ /** lit's `AsyncDirective`: can `setValue()` outside a render and observes connection. */
8
+ export abstract class AsyncDirective extends Directive {
9
+ constructor(partInfo: PartInfo);
10
+ /** Whether the host component is in the document. */
11
+ isConnected: boolean;
12
+ /** Commit a value to the part outside a render. A no-op on the server. */
13
+ setValue(value: unknown): void;
14
+ /** The host left the document, or the part was torn down. */
15
+ protected disconnected(): void;
16
+ /** The host came back. */
17
+ protected reconnected(): void;
18
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Async directive authoring, lit's `lit/async-directive.js` under a WebJs
3
+ * path: `@webjsdev/core/async-directive`.
4
+ *
5
+ * Portions derived from Lit, Copyright Google LLC, BSD-3-Clause.
6
+ *
7
+ * An `AsyncDirective` can push a value into its part outside a render with
8
+ * `setValue()`, and is told when the component that rendered it leaves the
9
+ * document (`disconnected()`) and comes back (`reconnected()`), so it can drop
10
+ * and re-establish a subscription.
11
+ *
12
+ * ```js
13
+ * import { AsyncDirective, directive } from '@webjsdev/core/async-directive';
14
+ *
15
+ * class Ticker extends AsyncDirective {
16
+ * render(ms) {
17
+ * this.#start(ms);
18
+ * return 0;
19
+ * }
20
+ * #start(ms) {
21
+ * clearInterval(this.id);
22
+ * let n = 0;
23
+ * this.id = setInterval(() => this.setValue(++n), ms);
24
+ * }
25
+ * disconnected() { clearInterval(this.id); }
26
+ * reconnected() { this.#start(1000); }
27
+ * }
28
+ * export const ticker = directive(Ticker);
29
+ * ```
30
+ *
31
+ * Connection follows the HOST component (the one whose template holds the
32
+ * part), plus the part's own lifetime: a directive whose part is torn down, or
33
+ * replaced by a different directive class, is disconnected for good. On the
34
+ * server `setValue()` does nothing and only `render()` runs.
35
+ *
36
+ * @module async-directive
37
+ */
38
+
39
+ import { Directive } from './directive.js';
40
+
41
+ export { directive, Directive, PartType } from './directive.js';
42
+
43
+ export class AsyncDirective extends Directive {
44
+ /** @param {{ type: number, name?: string, tagName?: string, strings?: readonly string[] }} partInfo */
45
+ constructor(partInfo) {
46
+ super(partInfo);
47
+ /**
48
+ * Whether the directive's host is in the document. `true` until the
49
+ * renderer reports otherwise.
50
+ * @type {boolean}
51
+ */
52
+ this.isConnected = true;
53
+ /**
54
+ * Installed by the client renderer: commits a value to this directive's
55
+ * part. Absent at SSR, where `setValue()` is a no-op.
56
+ * @type {((value: unknown) => void) | undefined}
57
+ */
58
+ this._$webjsCommit = undefined;
59
+ }
60
+
61
+ /**
62
+ * Commit `value` to the part outside a render, as if the directive had
63
+ * returned it. A no-op on the server. As in lit, stop calling it once
64
+ * `disconnected()` runs.
65
+ * @param {unknown} value
66
+ */
67
+ setValue(value) {
68
+ if (this._$webjsCommit) this._$webjsCommit(value);
69
+ }
70
+
71
+ /**
72
+ * Called by the renderer when the connection state changes; dispatches to
73
+ * `disconnected()` / `reconnected()` once per transition.
74
+ * @param {boolean} connected
75
+ */
76
+ _$webjsSetConnected(connected) {
77
+ if (connected === this.isConnected) return;
78
+ this.isConnected = connected;
79
+ if (connected) this.reconnected();
80
+ else this.disconnected();
81
+ }
82
+
83
+ /** The host left the document, or the part was torn down. Release resources. */
84
+ disconnected() {}
85
+
86
+ /** The host came back. Re-establish what `disconnected()` released. */
87
+ reconnected() {}
88
+ }