@proveanything/smartlinks 2.0.51 → 2.0.52

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.
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.51 | Generated: 2026-10-10T07:57:23.889Z
3
+ Version: 2.0.52 | Generated: 2026-10-11T13:21:58.062Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -62,7 +62,39 @@ Iframe Mode:
62
62
  **Where routing goes:**
63
63
  - ✅ Your **iframe entry point** (`App.tsx` / `main.tsx`) should use `HashRouter`
64
64
  - ✅ Your **exported container** (`PublicContainer.tsx`) should include `<Routes>` and `<Route>` elements
65
- - ❌ Your **exported container** should NOT include any `<Router>` wrapper
65
+ - ❌ Your **exported container** should NOT include any `<Router>` wrapper — *unless isolated* (below)
66
+
67
+ ### Pages that read their context from the URL: an isolated router
68
+
69
+ The example app's pages read `collectionId` / `productId` / `proofId` from the URL. The framework's
70
+ router starts at `/` without them, so the example's container keeps **its own** `MemoryRouter` (seeded
71
+ with those params) — and **isolates** it from the host's router first. Since R5, `react-router-dom` is a
72
+ shared dependency (the host's copy), so a plain nested router is the same library as the host's and
73
+ throws _"You cannot render a `<Router>` inside another `<Router>`"_. Resetting the parent's location and
74
+ navigation context gives your container a router of its own — the same isolation portal-framework puts
75
+ around every direct component:
76
+
77
+ ```tsx
78
+ import { MemoryRouter, Routes, Route, UNSAFE_LocationContext, UNSAFE_NavigationContext } from 'react-router-dom';
79
+
80
+ export const PublicContainer = ({ collectionId, appId, productId, proofId }: SmartLinksContainerProps) => {
81
+ const q = new URLSearchParams({ collectionId, appId, ...(productId ? { productId } : {}), ...(proofId ? { proofId } : {}) });
82
+ return (
83
+ <UNSAFE_LocationContext.Provider value={null as any}>
84
+ <UNSAFE_NavigationContext.Provider value={null as any}>
85
+ <MemoryRouter initialEntries={[`/?${q}`]}>
86
+ <Routes>
87
+ <Route path="/*" element={<PublicPage />} />
88
+ </Routes>
89
+ </MemoryRouter>
90
+ </UNSAFE_NavigationContext.Provider>
91
+ </UNSAFE_LocationContext.Provider>
92
+ );
93
+ };
94
+ ```
95
+
96
+ Your container's own navigation then stays inside it (it isn't mirrored into the portal's deep link).
97
+ `smartlinks-doctor` warns when a containers bundle uses a router without this isolation.
66
98
 
67
99
  ### The Router Contract
68
100
 
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.51 | Generated: 2026-10-10T07:57:23.889Z
3
+ Version: 2.0.52 | Generated: 2026-10-11T13:21:58.062Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -62,7 +62,39 @@ Iframe Mode:
62
62
  **Where routing goes:**
63
63
  - ✅ Your **iframe entry point** (`App.tsx` / `main.tsx`) should use `HashRouter`
64
64
  - ✅ Your **exported container** (`PublicContainer.tsx`) should include `<Routes>` and `<Route>` elements
65
- - ❌ Your **exported container** should NOT include any `<Router>` wrapper
65
+ - ❌ Your **exported container** should NOT include any `<Router>` wrapper — *unless isolated* (below)
66
+
67
+ ### Pages that read their context from the URL: an isolated router
68
+
69
+ The example app's pages read `collectionId` / `productId` / `proofId` from the URL. The framework's
70
+ router starts at `/` without them, so the example's container keeps **its own** `MemoryRouter` (seeded
71
+ with those params) — and **isolates** it from the host's router first. Since R5, `react-router-dom` is a
72
+ shared dependency (the host's copy), so a plain nested router is the same library as the host's and
73
+ throws _"You cannot render a `<Router>` inside another `<Router>`"_. Resetting the parent's location and
74
+ navigation context gives your container a router of its own — the same isolation portal-framework puts
75
+ around every direct component:
76
+
77
+ ```tsx
78
+ import { MemoryRouter, Routes, Route, UNSAFE_LocationContext, UNSAFE_NavigationContext } from 'react-router-dom';
79
+
80
+ export const PublicContainer = ({ collectionId, appId, productId, proofId }: SmartLinksContainerProps) => {
81
+ const q = new URLSearchParams({ collectionId, appId, ...(productId ? { productId } : {}), ...(proofId ? { proofId } : {}) });
82
+ return (
83
+ <UNSAFE_LocationContext.Provider value={null as any}>
84
+ <UNSAFE_NavigationContext.Provider value={null as any}>
85
+ <MemoryRouter initialEntries={[`/?${q}`]}>
86
+ <Routes>
87
+ <Route path="/*" element={<PublicPage />} />
88
+ </Routes>
89
+ </MemoryRouter>
90
+ </UNSAFE_NavigationContext.Provider>
91
+ </UNSAFE_LocationContext.Provider>
92
+ );
93
+ };
94
+ ```
95
+
96
+ Your container's own navigation then stays inside it (it isn't mirrored into the portal's deep link).
97
+ `smartlinks-doctor` warns when a containers bundle uses a router without this isolation.
66
98
 
67
99
  ### The Router Contract
68
100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "2.0.51",
3
+ "version": "2.0.52",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -168,16 +168,26 @@ if (meta.cssBaseline) {
168
168
  for (const c of ['sl-mobile-admin']) baseSet.add(c);
169
169
  // Class usage lives in compiled markup strings — present in every bundle regardless
170
170
  // of module format, so scan both umd + esm across all surfaces.
171
+ // `sl-…` NOT preceded by a word char or hyphen: a class name, never a CSS custom property
172
+ // (`var(--sl-color-accent)`, `--sl-widget-accent`) — those are theme tokens, not classes.
173
+ // Classes the app defines itself (`.sl-foo {…}` in its own CSS) aren't missing either.
171
174
  const used = new Set();
175
+ const defined = new Set();
172
176
  for (const [, block] of surfaces) {
173
- for (const p of [block?.files?.js?.umd, block?.files?.js?.esm]) {
174
- if (!p) continue;
177
+ for (const p of [block?.files?.js?.umd, block?.files?.js?.esm, block?.files?.css]) {
178
+ if (!p || typeof p !== 'string') continue;
175
179
  const bp = resolveBundle(p);
176
180
  if (!bp) continue;
177
- for (const m of readFileSync(bp, 'utf8').matchAll(/\bsl-[a-z0-9-]+/g)) used.add(m[0]);
181
+ const text = readFileSync(bp, 'utf8');
182
+ for (const m of text.matchAll(/(?<![\w-])sl-[a-z0-9-]+/g)) used.add(m[0]);
183
+ for (const m of text.matchAll(/\.(sl-[a-z0-9-]+)/g)) defined.add(m[1]);
178
184
  }
179
185
  }
180
- const unknown = [...used].filter((c) => !baseSet.has(c)).sort();
186
+ const ownSl = [...defined].filter((c) => !baseSet.has(c)).sort();
187
+ if (ownSl.length) {
188
+ warnings.push(`the app's own classes use the platform's sl- prefix: ${ownSl.join(', ')} — rename them (e.g. drop the "sl-") so they can't collide with a future baseline class.`);
189
+ }
190
+ const unknown = [...used].filter((c) => !baseSet.has(c) && !defined.has(c)).sort();
181
191
  if (used.size === 0) {
182
192
  console.log(`${DIM}cssBaseline (${baseline.version}) — declared; no sl-* classes found in the bundles.${RESET}`);
183
193
  } else if (unknown.length === 0) {
@@ -238,6 +248,51 @@ if (meta.respectsHostTheme) {
238
248
  }
239
249
  }
240
250
 
251
+ // ---- containers: a router of their own must be isolated from the host's ------------------------
252
+ // Since R5, react-router-dom is SHARED with the host, so a container that wraps itself in a
253
+ // MemoryRouter/HashRouter/BrowserRouter inside the host's router throws "You cannot render a <Router>
254
+ // inside another <Router>" — unless it resets the parent's router context first (docs/containers.md →
255
+ // "Pages that read their context from the URL"). Heuristic on the built bundle names; WARN-only.
256
+ {
257
+ const block = manifest.containers;
258
+ for (const p of [block?.files?.js?.esm, block?.files?.js?.umd]) {
259
+ if (!p) continue;
260
+ const bp = resolveBundle(p);
261
+ if (!bp) continue;
262
+ const text = readFileSync(bp, 'utf8');
263
+ const routers = ['MemoryRouter', 'HashRouter', 'BrowserRouter'].filter((r) => new RegExp(`\\b${r}\\b`).test(text));
264
+ if (!routers.length) { console.log(`${GREEN}✓${RESET} containers ${DIM}— no router of their own (the host's is used)${RESET}`); break; }
265
+ if (/UNSAFE_LocationContext/.test(text)) { console.log(`${GREEN}✓${RESET} containers ${DIM}— own ${routers.join('/')} isolated from the host's router${RESET}`); break; }
266
+ console.log(`${YELLOW}⚠${RESET} containers — ${routers.join('/')} without isolating it from the host's router`);
267
+ warnings.push(`containers bundle renders its own ${routers.join('/')} but never resets the host's router context — inside an R5 host that throws "You cannot render a <Router> inside another <Router>". Wrap it in UNSAFE_LocationContext.Provider + UNSAFE_NavigationContext.Provider value={null} (docs/containers.md), or drop the router and use the host's.`);
268
+ break;
269
+ }
270
+ }
271
+
272
+ // ---- package.json scripts point at files that exist ------------------------------------------
273
+ // An upgrade that copies a script LINE but not the script FILE breaks every build at that step
274
+ // (e.g. `postbuild: node scripts/smartlinks-deploy.mjs` with no scripts/smartlinks-deploy.mjs).
275
+ // WARN — the doctor runs inside the build, and a skipped hook (Forge builds skip pre/post) is harmless.
276
+ try {
277
+ const pkg = JSON.parse(readFileSync(join(appDir, 'package.json'), 'utf8'));
278
+ const missing = [];
279
+ for (const [name, cmd] of Object.entries(pkg.scripts || {})) {
280
+ if (typeof cmd !== 'string') continue;
281
+ for (const m of cmd.matchAll(/(?:^|[;&|]\s*|\s)(?:node|tsx|ts-node|bash|sh)\s+(?:-{1,2}[\w-]+(?:=\S+)?\s+)*((?:\.{1,2}\/)?[\w./-]+\.(?:mjs|cjs|js|ts|sh))\b/g)) {
282
+ if (!existsSync(join(appDir, m[1]))) missing.push(`"${name}" runs ${m[1]}`);
283
+ }
284
+ }
285
+ if (missing.length) {
286
+ console.log(`${YELLOW}⚠${RESET} package.json scripts — ${missing.length} point${missing.length === 1 ? 's' : ''} at a file that doesn't exist:`);
287
+ for (const x of missing) console.log(` ${YELLOW}${x}${RESET}`);
288
+ warnings.push(`package.json ${missing.join('; ')} — but the file is missing. Add the file, or remove the script (a pre/post hook like postbuild fails every \`npm run build\` without it).`);
289
+ } else {
290
+ console.log(`${GREEN}✓${RESET} package.json scripts ${DIM}— every script file they run exists${RESET}`);
291
+ }
292
+ } catch {
293
+ /* no package.json — not an npm app */
294
+ }
295
+
241
296
  console.log('');
242
297
  for (const w of warnings) console.log(`${YELLOW}⚠ ${w}${RESET}`);
243
298