@proveanything/smartlinks 2.0.51 → 2.0.53
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/dist/docs/API_SUMMARY.md +1 -1
- package/dist/docs/containers.md +33 -1
- package/docs/API_SUMMARY.md +1 -1
- package/docs/containers.md +33 -1
- package/package.json +1 -1
- package/scripts/doctor.mjs +59 -4
package/dist/docs/API_SUMMARY.md
CHANGED
package/dist/docs/containers.md
CHANGED
|
@@ -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/docs/API_SUMMARY.md
CHANGED
package/docs/containers.md
CHANGED
|
@@ -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
package/scripts/doctor.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 (/\bUNSAFE_LocationContext\b/.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
|
|