solidstep 0.1.7 → 0.2.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.
package/README.md CHANGED
@@ -364,8 +364,16 @@ export const options = {
364
364
  cache: {
365
365
  ttl: 60000, // Cache for 60 seconds
366
366
  },
367
+ responseHeaders: { // Custom headers for pages
368
+ 'X-Custom-Header': 'MyValue',
369
+ 'Cache-Control': 'public, max-age=60', // Client-side caching
370
+ },
367
371
  };
368
372
  ```
373
+ - Regarding caching, setting `ttl` to `0` or omitting it will disable caching for that page.
374
+ - Setting a positive integer value will cache the page for that duration in milliseconds.
375
+ - Invalidation of cached pages can be done using the `invalidateCache` and `revalidatePath` utilities.
376
+ - The `responseHeaders` option allows you to set custom HTTP headers for the page response.
369
377
 
370
378
  ## API Routes
371
379
 
@@ -406,6 +414,45 @@ const template = await fs.promises.readFile(TEMPLATE_PATH, 'utf-8');
406
414
 
407
415
  ## Utilities
408
416
 
417
+ ### Cache (Server-Side)
418
+ - Every page can be cached by setting the `options.cache` property in the page.
419
+ - You can also manually invalidate the cache for specific routes.
420
+ - Invalidation can be done in two ways:
421
+ 1. Using the `invalidateCache` utility to only invalidate paths.
422
+ ```tsx
423
+ import { invalidateCache } from 'solidstep/utils/cache';
424
+
425
+ const action = async () => {
426
+ 'use server';
427
+
428
+ ...
429
+
430
+ // Invalidate cache after data mutation
431
+ await invalidateCache('/some-route');
432
+
433
+ ...
434
+
435
+ return { success: true };
436
+ };
437
+ ```
438
+ 2. Using the `revalidatePath` utility to revalidate specific paths and revalidate the frontend DOM - signaling the server action as a Single Flight Mutation query.
439
+ ```tsx
440
+ import { revalidatePath } from 'solidstep/utils/cache';
441
+
442
+ const action = async () => {
443
+ 'use server';
444
+
445
+ ...
446
+
447
+ // Revalidate path after data mutation
448
+ await revalidatePath('/some-route');
449
+
450
+ ...
451
+
452
+ return { success: true };
453
+ };
454
+ ```
455
+
409
456
  ### Cookies
410
457
  ```tsx
411
458
  import { getCookie, setCookie } from 'solidstep/utils/cookies';
@@ -750,7 +797,6 @@ export const RootLayout = (props) => {
750
797
  As SolidStep is built using Vite, it follows the same guide as stated in [Vite docs](https://vite.dev/guide/env-and-mode) regarding environment variables.
751
798
 
752
799
  ## Future Plans
753
- - Revalidate on demand
754
800
  - Support for dynamic site.webmanifest, robots.txt, sitemap.xml, manifest.json, and llms.txt
755
801
  - Support loading and error pages for parallel routes
756
802
  - Support deferring loaders
package/client.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../client.ts"],"names":[],"mappings":"AACA,OAAO,cAAc,CAAC;AAYtB,eAAO,MAAM,IAAI,GACb,YAAY,MAAM,EAClB,cAAa,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EACxC,eAAc,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EACzC,qBAAoB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,kBAsF/C,CAAC;AAEF,eAAe,IAAI,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../client.ts"],"names":[],"mappings":"AACA,OAAO,cAAc,CAAC;AAwCtB,eAAO,MAAM,IAAI,GACb,YAAY,MAAM,EAClB,cAAa,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EACxC,eAAc,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EACzC,qBAAoB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,kBAsF/C,CAAC;AAEF,eAAe,IAAI,CAAC"}
package/client.js CHANGED
@@ -2,6 +2,32 @@ import { hydrate } from 'solid-js/web';
2
2
  import 'vinxi/client';
3
3
  import fileRoutes from 'vinxi/routes';
4
4
  import { getManifest } from 'vinxi/manifest';
5
+ import { createDiffDOM } from './utils/diff-dom';
6
+ window.onpageshow = () => {
7
+ const state = window.history.state;
8
+ const key = `${window.location.pathname}:build-time`;
9
+ const buildTimeMeta = document.querySelector('meta[name="x-build-time"]');
10
+ const buildTime = buildTimeMeta ? Number.parseInt(buildTimeMeta.getAttribute('content') || '0', 10) : 0;
11
+ const lastBuildTime = Number.parseInt(sessionStorage.getItem(key) || '0', 10);
12
+ const diffString = sessionStorage.getItem(window.location.pathname);
13
+ if (state?.revalidated && buildTime === lastBuildTime && diffString) {
14
+ // we need to re-apply the diff from session storage
15
+ const diff = JSON.parse(diffString);
16
+ const dd = createDiffDOM();
17
+ const didApply = dd.apply(document.body, diff);
18
+ if (didApply) {
19
+ const key = window.location.pathname;
20
+ sessionStorage.setItem(key, JSON.stringify(diff));
21
+ window.history.pushState({ revalidated: true }, '', window.location.href);
22
+ }
23
+ if (import.meta.env.DEV && !didApply) {
24
+ console.error('The mutation was not applied, this seems to be an edge case.');
25
+ console.error('Please raise an issue on GitHub describing your case.');
26
+ console.error('The diff calculated:', diff);
27
+ }
28
+ }
29
+ sessionStorage.setItem(key, buildTime.toString());
30
+ };
5
31
  const importModule = async (routeModule) => {
6
32
  const manifest = getManifest('client');
7
33
  if (import.meta.env.DEV) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solidstep",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "Next Step SolidJS Framework for building web applications.",
5
5
  "type": "module",
6
6
  "author": "HamzaKV <hamzakv333@gmail.com>",
package/server.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../server.ts"],"names":[],"mappings":"AAygBA,QAAA,MAAM,OAAO,+FAqUX,CAAC;AAEH,eAAe,OAAO,CAAC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../server.ts"],"names":[],"mappings":"AA0hBA,QAAA,MAAM,OAAO,+FAgVX,CAAC;AAEH,eAAe,OAAO,CAAC"}
package/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { eventHandler, toWebRequest } from 'vinxi/http';
1
+ import { eventHandler, toWebRequest, setResponseHeader, getEvent } from 'vinxi/http';
2
2
  import { getManifest } from 'vinxi/manifest';
3
3
  import { generateHydrationScript, renderToString } from 'solid-js/web';
4
4
  import fileRoutes, {} from 'vinxi/routes';
@@ -215,9 +215,18 @@ const sendNodeResponse = async (res, response) => {
215
215
  }
216
216
  };
217
217
  const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonce, }) => {
218
- const url = req.url || '/';
219
- const cachedEntry = getCache(url);
218
+ const url = new URL(req.url);
219
+ const path = url.pathname;
220
+ const cachedEntry = getCache(path);
220
221
  if (cachedEntry && toRender === 'main') {
222
+ const { options } = entry.mainPage.options ? await entry.mainPage.options.import() : { options: {} };
223
+ if (options?.responseHeaders) {
224
+ const headers = options.responseHeaders;
225
+ const event = getEvent();
226
+ for (const [key, value] of Object.entries(headers)) {
227
+ setResponseHeader(event, key, value);
228
+ }
229
+ }
221
230
  return {
222
231
  rendered: cachedEntry.rendered,
223
232
  documentMeta: cachedEntry.documentMeta,
@@ -316,6 +325,13 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
316
325
  if (options?.cache) {
317
326
  cachingOptions = options.cache;
318
327
  }
328
+ if (options?.responseHeaders) {
329
+ const headers = options.responseHeaders;
330
+ const event = getEvent();
331
+ for (const [key, value] of Object.entries(headers)) {
332
+ setResponseHeader(event, key, value);
333
+ }
334
+ }
319
335
  let data = {};
320
336
  if (pageLoader) {
321
337
  const result = await pageLoader.loader(req);
@@ -345,13 +361,14 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
345
361
  });
346
362
  const composed = await compose();
347
363
  const rendered = await renderToString(() => composed());
348
- if (cachingOptions && toRender === 'main') {
349
- setCache(url, {
364
+ if (toRender === 'main') {
365
+ const options = cachingOptions;
366
+ setCache(path, {
350
367
  rendered: rendered,
351
368
  documentMeta: meta,
352
369
  documentAssets: assets,
353
370
  loaderData: loaderData,
354
- }, cachingOptions.ttl);
371
+ }, options?.ttl ? options.ttl : 0);
355
372
  }
356
373
  return {
357
374
  rendered: rendered,
@@ -458,6 +475,14 @@ const handler = eventHandler(async (event) => {
458
475
  type: 'title',
459
476
  attributes: {},
460
477
  content: 'SolidStep'
478
+ },
479
+ build_time: {
480
+ type: 'meta',
481
+ attributes: {
482
+ name: 'x-build-time',
483
+ content: Date.now().toString(),
484
+ description: 'IMPORTANT: This tag indicates the build time of the application and should not be removed.'
485
+ },
461
486
  }
462
487
  };
463
488
  const assets = await clientManifest.inputs[clientManifest.handler].assets();
@@ -501,6 +526,9 @@ const handler = eventHandler(async (event) => {
501
526
  }
502
527
  else {
503
528
  try {
529
+ if (!matched.loadingPage) {
530
+ throw new Error('No loading page');
531
+ }
504
532
  const { rendered, documentMeta, documentAssets, loaderData, } = await render({
505
533
  toRender: 'loading',
506
534
  entry: matched,
package/utils/cache.d.ts CHANGED
@@ -2,4 +2,5 @@ export declare const getCache: <T>(key: string) => T | null;
2
2
  export declare const setCache: <T>(key: string, value: T, ttlMs?: number) => void;
3
3
  export declare const invalidateCache: (key: string) => void;
4
4
  export declare const clearAllCache: () => void;
5
+ export declare const revalidatePath: (path: string) => void;
5
6
  //# sourceMappingURL=cache.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../utils/cache.ts"],"names":[],"mappings":"AA6CA,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAE,KAAK,MAAM,KAAG,CAAC,GAAG,IAe7C,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAE,KAAK,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,MAAM,SA0BhE,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,SAU1C,CAAC;AAEF,eAAO,MAAM,aAAa,YAGzB,CAAC"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../utils/cache.ts"],"names":[],"mappings":"AA+CA,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAE,KAAK,MAAM,KAAG,CAAC,GAAG,IAe7C,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAE,KAAK,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,MAAM,SA0BhE,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,SAU1C,CAAC;AAEF,eAAO,MAAM,aAAa,YAGzB,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,SAS1C,CAAC"}
package/utils/cache.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { getEvent, setResponseHeader } from 'vinxi/http';
1
2
  const MAX_CACHE_ENTRIES = 1000;
2
3
  const cacheMap = new Map();
3
4
  let head;
@@ -36,7 +37,7 @@ const removeTail = () => {
36
37
  };
37
38
  export const getCache = (key) => {
38
39
  const entry = cacheMap.get(key);
39
- if (!entry)
40
+ if (!entry || !entry.expiresAt)
40
41
  return null;
41
42
  if (entry.expiresAt && entry.expiresAt < Date.now()) {
42
43
  cacheMap.delete(key);
@@ -95,3 +96,12 @@ export const clearAllCache = () => {
95
96
  cacheMap.clear();
96
97
  head = tail = undefined;
97
98
  };
99
+ export const revalidatePath = (path) => {
100
+ // get and verify the event
101
+ const event = getEvent();
102
+ if (!event.path.includes('_server')) {
103
+ throw new Error('This function can only be used in server functions.');
104
+ }
105
+ // add the revalidate header as a flag for the server action to do diffing
106
+ setResponseHeader(event, 'X-Revalidate', path);
107
+ };
@@ -0,0 +1,72 @@
1
+ declare const SKIP_MODES: {
2
+ readonly CHILDREN: "children";
3
+ readonly FULL: "full";
4
+ };
5
+ type SkipMode = (typeof SKIP_MODES)[keyof typeof SKIP_MODES];
6
+ type SkipPredicate = (domNode: Node | VirtualNode, virtualNode: VirtualNode) => boolean | SkipMode;
7
+ type PreDiffApplyHook = (info: {
8
+ diff: DiffResult;
9
+ node: Node;
10
+ }) => boolean | undefined;
11
+ type PostDiffApplyHook = (info: {
12
+ diff: DiffResult;
13
+ node: Node;
14
+ }) => void;
15
+ type FilterOuterDiffHook = (oldNode: VirtualNode, newNode: VirtualNode, diffs: DiffResult[]) => DiffResult[] | undefined;
16
+ type TextDiffHook = (target: Node, currentData: string, oldValue: string, newValue: string) => void;
17
+ interface DiffOptions {
18
+ skipSelector: string | null;
19
+ skipPredicate: SkipPredicate | null;
20
+ skipAttributes: string[];
21
+ skipChildren: boolean;
22
+ skipMode: SkipMode;
23
+ debug: boolean;
24
+ diffcap: number;
25
+ valueDiffing: boolean;
26
+ caseSensitive: boolean;
27
+ preVirtualDiffApply: PreDiffApplyHook | null;
28
+ postVirtualDiffApply: PostDiffApplyHook | null;
29
+ preDiffApply: PreDiffApplyHook | null;
30
+ postDiffApply: PostDiffApplyHook | null;
31
+ filterOuterDiff: FilterOuterDiffHook | null;
32
+ textDiff: TextDiffHook | null;
33
+ document: Document | null;
34
+ }
35
+ interface VirtualNode {
36
+ nodeType: number;
37
+ nodeName: string;
38
+ route?: number[];
39
+ data?: string;
40
+ attributes?: Record<string, string>;
41
+ value?: string;
42
+ checked?: boolean;
43
+ selected?: boolean;
44
+ childNodes?: VirtualNode[];
45
+ innerDone?: boolean;
46
+ skipFull?: boolean;
47
+ }
48
+ interface DiffResult {
49
+ action: string;
50
+ route: number[];
51
+ from?: number[];
52
+ to?: number[];
53
+ element?: VirtualNode;
54
+ index?: number;
55
+ name?: string;
56
+ value?: string;
57
+ oldValue?: string | boolean | VirtualNode;
58
+ newValue?: string | boolean | VirtualNode;
59
+ }
60
+ export declare const nodeToObj: (node: Node, options?: Partial<DiffOptions>) => VirtualNode | null;
61
+ export declare const stringToObj: (htmlString: string, options?: Partial<DiffOptions>) => VirtualNode;
62
+ export declare const objToNode: (obj: VirtualNode, options?: Partial<DiffOptions>) => Node | null;
63
+ export declare const diff: (elementA: string | Node | VirtualNode, elementB: string | Node | VirtualNode, options?: Partial<DiffOptions>) => DiffResult[];
64
+ export declare const apply: (element: Node, diffs: DiffResult[], options?: Partial<DiffOptions>) => boolean;
65
+ export declare const undo: (element: Node, diffs: DiffResult[], options?: Partial<DiffOptions>) => boolean;
66
+ export declare const createDiffDOM: (userOptions?: Partial<DiffOptions>) => {
67
+ diff: (elementA: string | Node | VirtualNode, elementB: string | Node | VirtualNode) => DiffResult[];
68
+ apply: (element: Node, diffs: DiffResult[]) => boolean;
69
+ undo: (element: Node, diffs: DiffResult[]) => boolean;
70
+ };
71
+ export {};
72
+ //# sourceMappingURL=diff-dom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-dom.d.ts","sourceRoot":"","sources":["../../utils/diff-dom.ts"],"names":[],"mappings":"AAsBA,QAAA,MAAM,UAAU;;;CAGN,CAAC;AAEX,KAAK,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAE7D,KAAK,aAAa,GAAG,CACjB,OAAO,EAAE,IAAI,GAAG,WAAW,EAC3B,WAAW,EAAE,WAAW,KACvB,OAAO,GAAG,QAAQ,CAAC;AACxB,KAAK,gBAAgB,GAAG,CAAC,IAAI,EAAE;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAE,KACzD,OAAO,GACP,SAAS,CAAC;AAChB,KAAK,iBAAiB,GAAG,CAAC,IAAI,EAAE;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAE,KAAK,IAAI,CAAC;AAC1E,KAAK,mBAAmB,GAAG,CACvB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,WAAW,EACpB,KAAK,EAAE,UAAU,EAAE,KAClB,UAAU,EAAE,GAAG,SAAS,CAAC;AAC9B,KAAK,YAAY,GAAG,CAChB,MAAM,EAAE,IAAI,EACZ,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,KACf,IAAI,CAAC;AAEV,UAAU,WAAW;IACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;IACpC,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,mBAAmB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC7C,oBAAoB,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC/C,YAAY,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACtC,aAAa,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACxC,eAAe,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC5C,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC7B;AAqBD,UAAU,WAAW;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,WAAW,EAAE,CAAC;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,UAAU,UAAU;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,WAAW,CAAC;IAC1C,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,WAAW,CAAC;CAC7C;AAqMD,eAAO,MAAM,SAAS,GAClB,MAAM,IAAI,EACV,UAAS,OAAO,CAAC,WAAW,CAAM,KACnC,WAAW,GAAG,IAoFhB,CAAC;AAEF,eAAO,MAAM,WAAW,GACpB,YAAY,MAAM,EAClB,UAAS,OAAO,CAAC,WAAW,CAAM,KACnC,WA8KF,CAAC;AAEF,eAAO,MAAM,SAAS,GAClB,KAAK,WAAW,EAChB,UAAS,OAAO,CAAC,WAAW,CAAM,KACnC,IAAI,GAAG,IA2DT,CAAC;AAkdF,eAAO,MAAM,IAAI,GACb,UAAU,MAAM,GAAG,IAAI,GAAG,WAAW,EACrC,UAAU,MAAM,GAAG,IAAI,GAAG,WAAW,EACrC,UAAS,OAAO,CAAC,WAAW,CAAM,KACnC,UAAU,EA+EZ,CAAC;AAyKF,eAAO,MAAM,KAAK,GACd,SAAS,IAAI,EACb,OAAO,UAAU,EAAE,EACnB,UAAS,OAAO,CAAC,WAAW,CAAM,KACnC,OAcF,CAAC;AAsDF,eAAO,MAAM,IAAI,GACb,SAAS,IAAI,EACb,OAAO,UAAU,EAAE,EACnB,UAAS,OAAO,CAAC,WAAW,CAAM,KACnC,OASF,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,cAAa,OAAO,CAAC,WAAW,CAAM;qBAK9C,MAAM,GAAG,IAAI,GAAG,WAAW,YAC3B,MAAM,GAAG,IAAI,GAAG,WAAW;qBAExB,IAAI,SAAS,UAAU,EAAE;oBAE1B,IAAI,SAAS,UAAU,EAAE;CAGhD,CAAC"}