@semi-solid/solid 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 semi-solid contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @semi-solid/solid
2
+
3
+ SolidJS runtime layer for Semi-Solid. Sits between the zero-dependency stubs in `@semi-solid/runtime` and your components, providing reactive implementations that depend on `solid-js`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @semi-solid/solid
9
+ ```
10
+
11
+ `solid-js ^1.9.0` is a peer dependency — install it alongside this package.
12
+
13
+ ## What's inside
14
+
15
+ | Export | Source | Description |
16
+ |--------|--------|-------------|
17
+ | `tap`, `tapWhen`, `tapRemote`, `tapPersonalized`, `liquidRaw`, `blockAttrs` | re-exported from `@semi-solid/runtime` | Compile-time stubs — the compiler replaces these with Liquid output and reactive signals |
18
+ | `t`, `setTranslations` | re-exported from `@semi-solid/runtime` | i18n translation helper |
19
+ | `createTapSignal`, `__setSectionId` | `tapWhen.ts` | Reactive signal that re-fetches from a Shopify data section when dependencies change |
20
+ | `__tapRemoteHtml` | `tapRemote.ts` | Fetches rendered section HTML from another route via the Section Rendering API |
21
+ | `createPersonalizedSignal`, `__setPersonalizationBaseUrl`, `buildUrl` | `tapPersonalized.ts` | Fetches personalized data from an external API with prefetch support |
22
+ | `createStore` | `store.ts` | localStorage-backed reactive store (wishlists, recently viewed, etc.) |
23
+
24
+ ## Usage
25
+
26
+ Components never import from this package directly. They use the `$lib/` Vite alias, which resolves to `packages/solid/src/`:
27
+
28
+ ```tsx
29
+ // src/snippets/ProductCard.tsx
30
+ import { tap } from '$lib/runtime';
31
+ import { t } from '$lib/i18n';
32
+
33
+ const title = tap('{{ product.title }}', 'Product');
34
+ ```
35
+
36
+ The compiler transforms `tap()` / `tapWhen()` / `tapPersonalized()` calls at build time. The reactive implementations (`createTapSignal`, `createPersonalizedSignal`, etc.) are auto-imported by the compiler into the cleaned output — you don't call them yourself.
37
+
38
+ ### createStore
39
+
40
+ `createStore` is the one export you use directly in component code:
41
+
42
+ ```tsx
43
+ import { createStore } from '$lib/runtime';
44
+
45
+ const recentlyViewed = createStore<Product>('recently-viewed', { maxItems: 10 });
46
+
47
+ // Read
48
+ const items = recentlyViewed.items(); // Accessor<Product[]>
49
+
50
+ // Write
51
+ recentlyViewed.add(product); // prepends, deduplicates, persists to localStorage
52
+ recentlyViewed.remove(product); // removes by JSON equality
53
+ recentlyViewed.clear(); // empties the store
54
+ ```
55
+
56
+ ## Architecture
57
+
58
+ ```
59
+ @semi-solid/runtime (zero dependencies — stubs + types)
60
+ |
61
+ @semi-solid/solid (solid-js — reactive implementations)
62
+ |
63
+ Components (import via $lib/ alias)
64
+ |
65
+ @semi-solid/compiler (Vite plugin — transforms source at build time)
66
+ ```
67
+
68
+ The compiler emits `$lib/runtime` and `$lib/tapWhen` imports in the transformed output. The `$lib` alias in `vite.config.ts` points to `packages/solid/src/`, so Vite resolves everything without any component files needing to know the physical path.
69
+
70
+ ## Development
71
+
72
+ ```bash
73
+ # Run tests
74
+ bun run test
75
+
76
+ # Watch mode
77
+ bun run test:watch
78
+ ```
@@ -0,0 +1,117 @@
1
+ export { blockAttrs, liquidRaw, setTranslations, t, tap, tapPersonalized, tapRemote, tapWhen } from '@semi-solid/runtime';
2
+ import { Accessor } from 'solid-js';
3
+
4
+ /**
5
+ * tapWhen runtime implementation.
6
+ *
7
+ * The compiler replaces every `tapWhen(liquidExpr, deps, fallback)` call with
8
+ * `createTapSignal(key, deps, fallback)`. The key is the LHS variable name,
9
+ * injected at compile time so the runtime knows which field to read from the
10
+ * JSON data section response.
11
+ *
12
+ * The data section is a Shopify section that renders only a
13
+ * <script type="application/json"> tag containing the component's tap-mapped
14
+ * values serialised with the | json Liquid filter. It can be fetched via:
15
+ *
16
+ * GET {any-route}?section_id={component-name}-data
17
+ *
18
+ * The currently-mounted component's section ID is communicated via a
19
+ * module-level variable set by the hydration entry immediately before
20
+ * calling render(). This is safe because render() is synchronous and
21
+ * createTapSignal() captures the value in a closure at call time.
22
+ */
23
+
24
+ declare function __setSectionId(id: string | null | undefined): void;
25
+ /**
26
+ * Creates a reactive signal for a tap-mapped value.
27
+ *
28
+ * - Initialises the signal from `initial` (the prop value from data-props).
29
+ * - Captures the active section ID at call time (synchronously set by the
30
+ * hydration entry before render()).
31
+ * - Registers a deferred createEffect that re-fetches the data section
32
+ * whenever any dep changes and updates the signal.
33
+ *
34
+ * @param key LHS variable name; used to index the JSON response.
35
+ * @param deps Signal accessors whose changes trigger a re-fetch.
36
+ * @param initial Initial value, usually props.xxx from data-props.
37
+ */
38
+ declare function createTapSignal<T>(key: string, deps: Accessor<unknown>[], initial: T): Accessor<T>;
39
+
40
+ /**
41
+ * tapRemote runtime implementation.
42
+ *
43
+ * The compiler replaces every `tapRemote(Component, url)` call with
44
+ * `__tapRemoteHtml("remote-{kebab-name}", url)`. This function creates a
45
+ * SolidJS signal, fetches the rendered section HTML from the given URL via
46
+ * the Shopify Section Rendering API, and returns an accessor for raw HTML
47
+ * injection.
48
+ *
49
+ * If `url` is a reactive accessor (function), the section is re-fetched
50
+ * whenever the URL changes.
51
+ */
52
+
53
+ declare function __tapRemoteHtml(sectionName: string, url: string | Accessor<string>): Accessor<string>;
54
+
55
+ /**
56
+ * tapPersonalized runtime implementation.
57
+ *
58
+ * The compiler replaces every `tapPersonalized(url, params, fallback)` call with
59
+ * `createPersonalizedSignal(url, params, fallback)`. This function creates a
60
+ * SolidJS signal, fetches personalized data from the external API, and returns
61
+ * an accessor.
62
+ *
63
+ * The compiler also generates:
64
+ * - <link rel="preconnect"> for the external domain
65
+ * - An inline <script> that starts the fetch early (prefetch), storing the
66
+ * promise on window.__p[url] so this runtime can pick it up without
67
+ * duplicating the request.
68
+ */
69
+
70
+ declare function __setPersonalizationBaseUrl(url: string | undefined): void;
71
+ /**
72
+ * Builds a full URL from the endpoint and params.
73
+ * - Resolves relative URLs against _personalizedBaseUrl
74
+ * - Sorts param keys alphabetically (deterministic cache key matching with prefetch)
75
+ * - Builds query string with encodeURIComponent
76
+ */
77
+ declare function buildUrl(endpoint: string, params: Record<string, unknown>): string;
78
+ declare global {
79
+ interface Window {
80
+ __p?: Record<string, Promise<unknown>>;
81
+ }
82
+ }
83
+ /**
84
+ * Creates a reactive signal for personalized data from an external API.
85
+ *
86
+ * 1. Checks window.__p[url] for a prefetched promise (started by the
87
+ * inline <head> script the compiler generates).
88
+ * 2. If no prefetch, fires a fetch immediately.
89
+ * 3. Sets up a deferred createEffect that re-fetches when any param
90
+ * signal changes.
91
+ *
92
+ * @param url API endpoint URL
93
+ * @param params Named params — may contain signal accessors or plain values
94
+ * @param initial Initial/fallback value
95
+ */
96
+ declare function createPersonalizedSignal<T>(url: string, params: Record<string, unknown>, initial: T): Accessor<T>;
97
+
98
+ /**
99
+ * createStore — a localStorage-backed reactive store using SolidJS signals.
100
+ *
101
+ * Useful for recently viewed items, wishlists, and other client-side
102
+ * persistence that should survive page navigations and browser restarts.
103
+ */
104
+
105
+ interface StoreOptions {
106
+ /** Maximum number of items to keep. Default: 20 */
107
+ maxItems?: number;
108
+ }
109
+ interface PersistentStore<T> {
110
+ items: Accessor<T[]>;
111
+ add: (item: T) => void;
112
+ remove: (item: T) => void;
113
+ clear: () => void;
114
+ }
115
+ declare function createStore<T>(key: string, options?: StoreOptions): PersistentStore<T>;
116
+
117
+ export { __setPersonalizationBaseUrl, __setSectionId, __tapRemoteHtml, buildUrl, createPersonalizedSignal, createStore, createTapSignal };
package/dist/index.js ADDED
@@ -0,0 +1,200 @@
1
+ // src/index.ts
2
+ import { tap, tapWhen, liquidRaw, blockAttrs, tapRemote, tapPersonalized } from "@semi-solid/runtime";
3
+ import { t, setTranslations } from "@semi-solid/runtime";
4
+
5
+ // src/tapWhen.ts
6
+ import { createSignal, createEffect, on } from "solid-js";
7
+ var _activeSectionId;
8
+ function __setSectionId(id) {
9
+ _activeSectionId = id ?? void 0;
10
+ }
11
+ async function fetchSectionData(sectionId) {
12
+ const url = new URL(
13
+ window.location.pathname + window.location.search,
14
+ window.location.origin
15
+ );
16
+ url.searchParams.set("section_id", sectionId);
17
+ const html = await fetch(url.toString()).then((r) => r.text());
18
+ const doc = new DOMParser().parseFromString(html, "text/html");
19
+ const script = doc.querySelector('script[type="application/json"]');
20
+ return JSON.parse(script?.textContent ?? "{}");
21
+ }
22
+ function createTapSignal(key, deps, initial) {
23
+ const sectionId = _activeSectionId;
24
+ const [value, setValue] = createSignal(initial);
25
+ if (sectionId && deps.length > 0) {
26
+ createEffect(
27
+ on(
28
+ deps,
29
+ async () => {
30
+ try {
31
+ const data = await fetchSectionData(sectionId);
32
+ if (key in data) setValue(() => data[key]);
33
+ } catch (e) {
34
+ console.error(`[tapWhen] failed to refresh "${key}":`, e);
35
+ }
36
+ },
37
+ { defer: true }
38
+ // skip initial run — data-props covers the first render
39
+ )
40
+ );
41
+ }
42
+ return value;
43
+ }
44
+
45
+ // src/tapRemote.ts
46
+ import { createSignal as createSignal2, createEffect as createEffect2, on as on2 } from "solid-js";
47
+ async function fetchSection(sectionName, url) {
48
+ const target = new URL(url, window.location.origin);
49
+ target.searchParams.set("section_id", sectionName);
50
+ const html = await fetch(target.toString()).then((r) => r.text());
51
+ const doc = new DOMParser().parseFromString(html, "text/html");
52
+ const wrapper = doc.querySelector('[id^="shopify-section-"]');
53
+ return wrapper?.innerHTML ?? html;
54
+ }
55
+ function __tapRemoteHtml(sectionName, url) {
56
+ const [html, setHtml] = createSignal2("");
57
+ if (typeof url === "function") {
58
+ createEffect2(
59
+ on2(url, async (currentUrl) => {
60
+ try {
61
+ const result = await fetchSection(sectionName, currentUrl);
62
+ setHtml(() => result);
63
+ } catch (e) {
64
+ console.error(`[tapRemote] failed to fetch "${sectionName}":`, e);
65
+ }
66
+ })
67
+ );
68
+ } else {
69
+ fetchSection(sectionName, url).then(
70
+ (result) => setHtml(() => result),
71
+ (e) => console.error(`[tapRemote] failed to fetch "${sectionName}":`, e)
72
+ );
73
+ }
74
+ return html;
75
+ }
76
+
77
+ // src/tapPersonalized.ts
78
+ import { createSignal as createSignal3, createEffect as createEffect3, on as on3 } from "solid-js";
79
+ var _personalizedBaseUrl;
80
+ function __setPersonalizationBaseUrl(url) {
81
+ _personalizedBaseUrl = url;
82
+ }
83
+ function buildUrl(endpoint, params) {
84
+ let base;
85
+ if (/^https?:\/\//.test(endpoint)) {
86
+ base = endpoint;
87
+ } else {
88
+ const origin = _personalizedBaseUrl ?? "";
89
+ base = origin.replace(/\/$/, "") + "/" + endpoint.replace(/^\//, "");
90
+ }
91
+ const keys = Object.keys(params).sort();
92
+ if (keys.length === 0) return base;
93
+ const qs = keys.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(String(params[k] ?? ""))}`).join("&");
94
+ return `${base}?${qs}`;
95
+ }
96
+ function createPersonalizedSignal(url, params, initial) {
97
+ const [value, setValue] = createSignal3(initial);
98
+ function resolveParams() {
99
+ const resolved = {};
100
+ for (const [k, v] of Object.entries(params)) {
101
+ resolved[k] = typeof v === "function" ? v() : v;
102
+ }
103
+ return resolved;
104
+ }
105
+ async function fetchData() {
106
+ const resolved = resolveParams();
107
+ const fullUrl = buildUrl(url, resolved);
108
+ try {
109
+ const prefetch = window.__p?.[fullUrl];
110
+ let data;
111
+ if (prefetch) {
112
+ data = await prefetch;
113
+ delete window.__p[fullUrl];
114
+ } else {
115
+ const res = await fetch(fullUrl);
116
+ data = await res.json();
117
+ }
118
+ setValue(() => data);
119
+ } catch (e) {
120
+ console.error(`[tapPersonalized] failed to fetch "${url}":`, e);
121
+ }
122
+ }
123
+ const accessors = Object.values(params).filter(
124
+ (v) => typeof v === "function"
125
+ );
126
+ if (accessors.length > 0) {
127
+ fetchData();
128
+ createEffect3(
129
+ on3(
130
+ accessors,
131
+ () => {
132
+ fetchData();
133
+ },
134
+ { defer: true }
135
+ )
136
+ );
137
+ } else {
138
+ fetchData();
139
+ }
140
+ return value;
141
+ }
142
+
143
+ // src/store.ts
144
+ import { createSignal as createSignal4 } from "solid-js";
145
+ function readStorage(key) {
146
+ try {
147
+ const raw = localStorage.getItem(key);
148
+ if (!raw) return [];
149
+ return JSON.parse(raw);
150
+ } catch {
151
+ return [];
152
+ }
153
+ }
154
+ function writeStorage(key, value) {
155
+ try {
156
+ localStorage.setItem(key, JSON.stringify(value));
157
+ } catch {
158
+ }
159
+ }
160
+ function createStore(key, options) {
161
+ const maxItems = options?.maxItems ?? 20;
162
+ const initial = readStorage(key);
163
+ const [items, setItems] = createSignal4(initial);
164
+ function persist(next) {
165
+ setItems(() => next);
166
+ writeStorage(key, next);
167
+ }
168
+ function add(item) {
169
+ const serialised = JSON.stringify(item);
170
+ const deduped = items().filter((existing) => JSON.stringify(existing) !== serialised);
171
+ const next = [item, ...deduped].slice(0, maxItems);
172
+ persist(next);
173
+ }
174
+ function remove(item) {
175
+ const serialised = JSON.stringify(item);
176
+ const next = items().filter((existing) => JSON.stringify(existing) !== serialised);
177
+ persist(next);
178
+ }
179
+ function clear() {
180
+ persist([]);
181
+ }
182
+ return { items, add, remove, clear };
183
+ }
184
+ export {
185
+ __setPersonalizationBaseUrl,
186
+ __setSectionId,
187
+ __tapRemoteHtml,
188
+ blockAttrs,
189
+ buildUrl,
190
+ createPersonalizedSignal,
191
+ createStore,
192
+ createTapSignal,
193
+ liquidRaw,
194
+ setTranslations,
195
+ t,
196
+ tap,
197
+ tapPersonalized,
198
+ tapRemote,
199
+ tapWhen
200
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@semi-solid/solid",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "LICENSE"
17
+ ],
18
+ "dependencies": {
19
+ "@semi-solid/runtime": "0.1.0"
20
+ },
21
+ "peerDependencies": {
22
+ "solid-js": "^1.9.0"
23
+ },
24
+ "devDependencies": {
25
+ "tsup": "^8.5.1",
26
+ "typescript": "^5.5.0",
27
+ "vitest": "^2.0.0"
28
+ },
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "test": "vitest run",
32
+ "test:watch": "vitest"
33
+ }
34
+ }