@ahmednawaz/crank 0.2.7 → 0.2.8
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/CLAUDE.md +3 -1
- package/bookmarklet/crank-bookmarklet.js +42 -4
- package/lib/init.js +45 -11
- package/lib/loader-url.js +16 -1
- package/package.json +1 -1
- package/react.d.ts +8 -3
- package/react.js +72 -12
- package/react.mjs +73 -19
- package/vite.js +38 -12
package/CLAUDE.md
CHANGED
|
@@ -58,7 +58,9 @@ Visual copy tooling that persists text changes to source via the coding agent (M
|
|
|
58
58
|
- Secrets: `CRANK_TEAM_TOKEN`
|
|
59
59
|
- Workflow Run: `npx @ahmednawaz/crank start`
|
|
60
60
|
- Ports: **3344** (panel), **3345** (`/mcp`)
|
|
61
|
-
-
|
|
61
|
+
- Panel loads via same-origin Vite proxy `/@crank/*` (setup registers `crank({ inject: false })`). Restart **Vite** after setup.
|
|
62
|
+
- Fallback: `window.__CRANK_URL__` / `VITE_CRANK_URL` = public `https://3344-…` URL
|
|
63
|
+
- After closing the panel (×), a **Crank** chip appears bottom-right to reopen.
|
|
62
64
|
|
|
63
65
|
## Apply workflow
|
|
64
66
|
|
|
@@ -96,10 +96,15 @@
|
|
|
96
96
|
function detectCompanionOrigin() {
|
|
97
97
|
try {
|
|
98
98
|
const scripts = document.querySelectorAll(
|
|
99
|
-
'script[src*="bookmarklet.js"],script[src*="dev-loader.js"]'
|
|
99
|
+
'script[src*="bookmarklet.js"],script[src*="dev-loader.js"],script[data-crank-loader="1"]'
|
|
100
100
|
);
|
|
101
101
|
for (const s of scripts) {
|
|
102
|
-
|
|
102
|
+
const u = new URL(s.src, location.href);
|
|
103
|
+
// Vite same-origin proxy: /@crank/dev-loader.js → companion base is origin+/@crank
|
|
104
|
+
if (u.pathname.indexOf('/@crank/') === 0 || u.pathname === '/@crank') {
|
|
105
|
+
return u.origin + '/@crank';
|
|
106
|
+
}
|
|
107
|
+
return u.origin;
|
|
103
108
|
}
|
|
104
109
|
} catch (_) {}
|
|
105
110
|
return null;
|
|
@@ -2256,14 +2261,47 @@
|
|
|
2256
2261
|
document.removeEventListener('keydown', onKey, true);
|
|
2257
2262
|
}
|
|
2258
2263
|
|
|
2264
|
+
function showReopenChip() {
|
|
2265
|
+
if (document.getElementById('vp-crank-reopen')) {
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
const chip = document.createElement('button');
|
|
2269
|
+
chip.id = 'vp-crank-reopen';
|
|
2270
|
+
chip.type = 'button';
|
|
2271
|
+
chip.textContent = 'Crank';
|
|
2272
|
+
chip.setAttribute('aria-label', 'Reopen Crank panel');
|
|
2273
|
+
chip.style.cssText =
|
|
2274
|
+
'position:fixed;bottom:16px;right:16px;z-index:2147483646;padding:8px 12px;' +
|
|
2275
|
+
'border-radius:999px;border:1px solid rgba(0,0,0,.12);background:#0f766e;color:#fff;' +
|
|
2276
|
+
'font:600 12px/1.2 system-ui,sans-serif;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.18);';
|
|
2277
|
+
chip.addEventListener('click', function () {
|
|
2278
|
+
chip.remove();
|
|
2279
|
+
window.__CRANK_LOADER__ = false;
|
|
2280
|
+
window.__crankActive = false;
|
|
2281
|
+
const s = document.createElement('script');
|
|
2282
|
+
s.src = COMPANION_HTTP + '/bookmarklet.js?t=' + Date.now();
|
|
2283
|
+
s.async = true;
|
|
2284
|
+
s.dataset.crankLoader = '1';
|
|
2285
|
+
document.documentElement.appendChild(s);
|
|
2286
|
+
});
|
|
2287
|
+
document.documentElement.appendChild(chip);
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2259
2290
|
function teardown() {
|
|
2260
2291
|
stopSelect();
|
|
2261
2292
|
restoreOriginal();
|
|
2262
2293
|
if (socket) socket.close();
|
|
2263
|
-
panel.remove();
|
|
2264
|
-
highlight.remove();
|
|
2294
|
+
if (panel && panel.parentNode) panel.remove();
|
|
2295
|
+
if (highlight && highlight.parentNode) highlight.remove();
|
|
2265
2296
|
window.__crankActive = false;
|
|
2266
2297
|
window.__crankTeardown = null;
|
|
2298
|
+
window.__CRANK_LOADER__ = false;
|
|
2299
|
+
try {
|
|
2300
|
+
document.querySelectorAll('script[data-crank-loader="1"]').forEach(function (el) {
|
|
2301
|
+
el.remove();
|
|
2302
|
+
});
|
|
2303
|
+
} catch (_) {}
|
|
2304
|
+
showReopenChip();
|
|
2267
2305
|
}
|
|
2268
2306
|
|
|
2269
2307
|
window.__crankTeardown = teardown;
|
package/lib/init.js
CHANGED
|
@@ -228,20 +228,33 @@ function missingNodeImportLines(source) {
|
|
|
228
228
|
return NODE_IMPORT_SPECS.filter(({ re }) => !re.test(source)).map(({ line }) => line);
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
function ensureCrankInPlugins(source) {
|
|
232
|
-
|
|
231
|
+
function ensureCrankInPlugins(source, options = {}) {
|
|
232
|
+
const call = options.inject === false ? 'crank({ inject: false })' : 'crank()';
|
|
233
|
+
const already =
|
|
234
|
+
/\bcrank\s*\?\s*\[\s*crank\s*\(/.test(source) ||
|
|
235
|
+
/\[\s*\.\.\.\s*\(\s*crank\s*\?/.test(source);
|
|
236
|
+
if (already) {
|
|
237
|
+
// Upgrade bare crank() → crank({ inject: false }) when requested (React + proxy).
|
|
238
|
+
if (options.inject === false && /crank\(\s*\)/.test(source) && !/crank\(\s*\{\s*inject\s*:/.test(source)) {
|
|
239
|
+
return {
|
|
240
|
+
source: source.replace(/crank\(\s*\)/g, 'crank({ inject: false })'),
|
|
241
|
+
changed: true,
|
|
242
|
+
ok: true
|
|
243
|
+
};
|
|
244
|
+
}
|
|
233
245
|
return { source, changed: false, ok: true };
|
|
234
246
|
}
|
|
247
|
+
const pluginsExpr = `[...(crank ? [${call}] : [])]`;
|
|
235
248
|
if (/plugins\s*:\s*\[\s*\]/.test(source)) {
|
|
236
249
|
return {
|
|
237
|
-
source: source.replace(/plugins\s*:\s*\[\s*\]/,
|
|
250
|
+
source: source.replace(/plugins\s*:\s*\[\s*\]/, `plugins: ${pluginsExpr}`),
|
|
238
251
|
changed: true,
|
|
239
252
|
ok: true
|
|
240
253
|
};
|
|
241
254
|
}
|
|
242
255
|
if (/plugins\s*:\s*\[/.test(source)) {
|
|
243
256
|
return {
|
|
244
|
-
source: source.replace(/plugins\s*:\s*\[/,
|
|
257
|
+
source: source.replace(/plugins\s*:\s*\[/, `plugins: [...(crank ? [${call}] : []), `),
|
|
245
258
|
changed: true,
|
|
246
259
|
ok: true
|
|
247
260
|
};
|
|
@@ -250,7 +263,7 @@ function ensureCrankInPlugins(source) {
|
|
|
250
263
|
return {
|
|
251
264
|
source: source.replace(
|
|
252
265
|
/defineConfig\s*\(\s*\{/,
|
|
253
|
-
|
|
266
|
+
`defineConfig({\n plugins: ${pluginsExpr},`
|
|
254
267
|
),
|
|
255
268
|
changed: true,
|
|
256
269
|
ok: true
|
|
@@ -260,7 +273,7 @@ function ensureCrankInPlugins(source) {
|
|
|
260
273
|
return {
|
|
261
274
|
source: source.replace(
|
|
262
275
|
/export\s+default\s+\{/,
|
|
263
|
-
|
|
276
|
+
`export default {\n plugins: ${pluginsExpr},`
|
|
264
277
|
),
|
|
265
278
|
changed: true,
|
|
266
279
|
ok: true
|
|
@@ -270,7 +283,7 @@ function ensureCrankInPlugins(source) {
|
|
|
270
283
|
return {
|
|
271
284
|
source: source.replace(
|
|
272
285
|
/module\.exports\s*=\s*\{/,
|
|
273
|
-
|
|
286
|
+
`module.exports = {\n plugins: ${pluginsExpr},`
|
|
274
287
|
),
|
|
275
288
|
changed: true,
|
|
276
289
|
ok: true
|
|
@@ -279,7 +292,7 @@ function ensureCrankInPlugins(source) {
|
|
|
279
292
|
return { source, changed: false, ok: false };
|
|
280
293
|
}
|
|
281
294
|
|
|
282
|
-
function patchViteConfig(projectRoot) {
|
|
295
|
+
function patchViteConfig(projectRoot, options = {}) {
|
|
283
296
|
const configPath = findViteConfig(projectRoot);
|
|
284
297
|
const stubPath = writeVitePluginStub(projectRoot);
|
|
285
298
|
|
|
@@ -297,6 +310,17 @@ function patchViteConfig(projectRoot) {
|
|
|
297
310
|
}
|
|
298
311
|
|
|
299
312
|
if (hasCrankVitePatch(source)) {
|
|
313
|
+
const upgraded = ensureCrankInPlugins(source, options);
|
|
314
|
+
if (upgraded.changed) {
|
|
315
|
+
source = dedupeCrankNodeImports(upgraded.source).source;
|
|
316
|
+
fs.writeFileSync(configPath, source);
|
|
317
|
+
return {
|
|
318
|
+
patched: true,
|
|
319
|
+
reason: 'upgraded-inject-false',
|
|
320
|
+
file: configPath,
|
|
321
|
+
plugin: stubPath
|
|
322
|
+
};
|
|
323
|
+
}
|
|
300
324
|
return {
|
|
301
325
|
patched: repaired.changed,
|
|
302
326
|
reason: repaired.changed ? 'repaired-duplicate-imports' : 'already-present',
|
|
@@ -338,7 +362,7 @@ try {
|
|
|
338
362
|
source = gracefulBlock + source;
|
|
339
363
|
}
|
|
340
364
|
|
|
341
|
-
const plugged = ensureCrankInPlugins(source);
|
|
365
|
+
const plugged = ensureCrankInPlugins(source, options);
|
|
342
366
|
if (!plugged.ok) {
|
|
343
367
|
return {
|
|
344
368
|
patched: false,
|
|
@@ -359,7 +383,7 @@ try {
|
|
|
359
383
|
if (!/vite-plugin\.cjs/.test(source) && !/_crankPluginPath/.test(source)) {
|
|
360
384
|
source = gracefulBlock + source;
|
|
361
385
|
}
|
|
362
|
-
const plugged = ensureCrankInPlugins(source);
|
|
386
|
+
const plugged = ensureCrankInPlugins(source, options);
|
|
363
387
|
if (!plugged.ok) {
|
|
364
388
|
return {
|
|
365
389
|
patched: false,
|
|
@@ -628,7 +652,17 @@ function initProject(projectRoot, options = {}) {
|
|
|
628
652
|
if (looksLikeReact && options.react !== false) {
|
|
629
653
|
writeReactMountHint(projectRoot, env, results);
|
|
630
654
|
results.reactMount = true;
|
|
631
|
-
|
|
655
|
+
// Still register Vite plugin with inject:false — enables /@crank proxy (Replit iframe).
|
|
656
|
+
if (env.hasVite && options.vite !== false) {
|
|
657
|
+
const viteResult = patchViteConfig(projectRoot, { inject: false });
|
|
658
|
+
results.vite = viteResult;
|
|
659
|
+
if (viteResult.patched || viteResult.reason === 'repaired-duplicate-imports') {
|
|
660
|
+
results.wrote.push(path.relative(projectRoot, viteResult.file));
|
|
661
|
+
}
|
|
662
|
+
if (viteResult.plugin) {
|
|
663
|
+
results.wrote.push(path.relative(projectRoot, viteResult.plugin));
|
|
664
|
+
}
|
|
665
|
+
} else if (env.hasVite) {
|
|
632
666
|
const repaired = repairViteConfigDuplicates(projectRoot);
|
|
633
667
|
if (repaired.repaired) {
|
|
634
668
|
results.vite = repaired;
|
package/lib/loader-url.js
CHANGED
|
@@ -31,6 +31,11 @@ function resolveCompanionLoaderUrl(options = {}) {
|
|
|
31
31
|
return String(options.src);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Prefer same-origin Vite proxy path (works inside Replit iframe / CSP).
|
|
35
|
+
if (options.preferProxy !== false && !options.companionUrl && !options.absolute) {
|
|
36
|
+
return '/@crank/dev-loader.js';
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
const projectRoot = options.projectRoot || findProjectRoot(process.cwd());
|
|
35
40
|
const runtime = readRuntimeManifest(projectRoot);
|
|
36
41
|
if (runtime?.companion?.devLoader) {
|
|
@@ -49,7 +54,17 @@ function resolveCompanionLoaderUrl(options = {}) {
|
|
|
49
54
|
return `${base}/dev-loader.js`;
|
|
50
55
|
}
|
|
51
56
|
|
|
57
|
+
/** Internal companion base for Vite `server.proxy` target (always loopback). */
|
|
58
|
+
function resolveCompanionProxyTarget(options = {}) {
|
|
59
|
+
if (options.target) {
|
|
60
|
+
return String(options.target).replace(/\/$/, '');
|
|
61
|
+
}
|
|
62
|
+
const port = String(options.port || env('PORT', '3344')).replace(/[^0-9]/g, '') || '3344';
|
|
63
|
+
return `http://127.0.0.1:${port}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
module.exports = {
|
|
53
67
|
findProjectRoot,
|
|
54
|
-
resolveCompanionLoaderUrl
|
|
68
|
+
resolveCompanionLoaderUrl,
|
|
69
|
+
resolveCompanionProxyTarget
|
|
55
70
|
};
|
package/package.json
CHANGED
package/react.d.ts
CHANGED
|
@@ -11,15 +11,20 @@ export interface CrankProps {
|
|
|
11
11
|
|
|
12
12
|
export declare function resolveCrankCompanionUrl(props?: CrankProps): string;
|
|
13
13
|
export declare function resolveCrankLoaderSrc(props?: CrankProps): string;
|
|
14
|
+
/** Infer https://3344-… from the current Replit preview hostname. */
|
|
15
|
+
export declare function detectReplitCompanionUrl(): string | null;
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
*
|
|
18
|
+
* Mount — injects companion loader once. Renders null.
|
|
19
|
+
*
|
|
20
|
+
* Prefers same-origin `/@crank/dev-loader.js` (Vite proxy) so the panel works
|
|
21
|
+
* inside Replit iframe previews; falls back to public companion / localhost.
|
|
17
22
|
*
|
|
18
23
|
* import { Crank } from '@ahmednawaz/crank/react'
|
|
19
24
|
* <Crank />
|
|
20
25
|
*
|
|
21
|
-
* Optional: `window.__CRANK_URL__` or `VITE_CRANK_URL
|
|
22
|
-
*
|
|
26
|
+
* Optional: `window.__CRANK_URL__` or `VITE_CRANK_URL`. Keep companion up:
|
|
27
|
+
* `npx @ahmednawaz/crank start`
|
|
23
28
|
*/
|
|
24
29
|
export declare function Crank(props?: CrankProps): ReactElement | null;
|
|
25
30
|
export declare const CrankLoader: typeof Crank;
|
package/react.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* CJS build of @ahmednawaz/crank/react (browser-safe, no Node/fs).
|
|
5
|
-
* Vite / modern apps should import the ESM build via package exports "import" condition.
|
|
6
5
|
*/
|
|
7
6
|
|
|
7
|
+
const PROXY_LOADER = '/@crank/dev-loader.js';
|
|
8
|
+
|
|
8
9
|
function getReact() {
|
|
9
10
|
try {
|
|
10
11
|
return require('react');
|
|
@@ -15,6 +16,22 @@ function getReact() {
|
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
function detectReplitCompanionUrl() {
|
|
20
|
+
if (typeof window === 'undefined') {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const host = window.location.hostname || '';
|
|
25
|
+
if (!/\.replit\.dev$|\.repl\.co$|\.replit\.app$/.test(host)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const base = host.replace(/^\d+-/, '');
|
|
29
|
+
return 'https://3344-' + base;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
function resolveCrankCompanionUrl(props) {
|
|
19
36
|
props = props || {};
|
|
20
37
|
if (typeof window !== 'undefined' && window.__CRANK_URL__) {
|
|
@@ -26,6 +43,23 @@ function resolveCrankCompanionUrl(props) {
|
|
|
26
43
|
if (props.url) {
|
|
27
44
|
return String(props.url).replace(/\/$/, '');
|
|
28
45
|
}
|
|
46
|
+
try {
|
|
47
|
+
if (
|
|
48
|
+
typeof process !== 'undefined' &&
|
|
49
|
+
process.env &&
|
|
50
|
+
(process.env.VITE_CRANK_URL || process.env.VITE_CRANK_COMPANION_URL)
|
|
51
|
+
) {
|
|
52
|
+
return String(
|
|
53
|
+
process.env.VITE_CRANK_URL || process.env.VITE_CRANK_COMPANION_URL
|
|
54
|
+
).replace(/\/$/, '');
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// ignore
|
|
58
|
+
}
|
|
59
|
+
const replit = detectReplitCompanionUrl();
|
|
60
|
+
if (replit) {
|
|
61
|
+
return replit;
|
|
62
|
+
}
|
|
29
63
|
return 'http://localhost:3344';
|
|
30
64
|
}
|
|
31
65
|
|
|
@@ -34,32 +68,57 @@ function resolveCrankLoaderSrc(props) {
|
|
|
34
68
|
if (props.src) {
|
|
35
69
|
return String(props.src);
|
|
36
70
|
}
|
|
37
|
-
|
|
71
|
+
if (
|
|
72
|
+
typeof window !== 'undefined' &&
|
|
73
|
+
!props.companionUrl &&
|
|
74
|
+
!props.url &&
|
|
75
|
+
!(typeof window !== 'undefined' && window.__CRANK_URL__)
|
|
76
|
+
) {
|
|
77
|
+
return PROXY_LOADER;
|
|
78
|
+
}
|
|
79
|
+
return resolveCrankCompanionUrl(props) + '/dev-loader.js';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function appendLoaderScript(src, onError) {
|
|
83
|
+
const script = document.createElement('script');
|
|
84
|
+
script.src = src.indexOf('?') >= 0 ? src : src + '?t=' + Date.now();
|
|
85
|
+
script.async = true;
|
|
86
|
+
script.dataset.crankLoader = '1';
|
|
87
|
+
if (onError) {
|
|
88
|
+
script.onerror = onError;
|
|
89
|
+
}
|
|
90
|
+
document.documentElement.appendChild(script);
|
|
91
|
+
return script;
|
|
38
92
|
}
|
|
39
93
|
|
|
40
94
|
function Crank(props) {
|
|
41
95
|
const React = getReact();
|
|
42
96
|
props = props || {};
|
|
43
|
-
const
|
|
97
|
+
const preferredSrc = resolveCrankLoaderSrc(props);
|
|
44
98
|
|
|
45
|
-
React.useEffect(()
|
|
99
|
+
React.useEffect(function () {
|
|
46
100
|
if (typeof document === 'undefined') {
|
|
47
101
|
return undefined;
|
|
48
102
|
}
|
|
49
|
-
if (document.
|
|
103
|
+
if (document.getElementById('vp-shell') || document.getElementById('vp-crank-reopen')) {
|
|
50
104
|
return undefined;
|
|
51
105
|
}
|
|
52
|
-
if (
|
|
106
|
+
if (document.querySelector('script[data-crank-loader="1"]')) {
|
|
53
107
|
return undefined;
|
|
54
108
|
}
|
|
55
109
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
110
|
+
const fallback = resolveCrankCompanionUrl(props) + '/dev-loader.js';
|
|
111
|
+
appendLoaderScript(preferredSrc, function () {
|
|
112
|
+
if (preferredSrc !== fallback && !document.getElementById('vp-shell')) {
|
|
113
|
+
document.querySelectorAll('script[data-crank-loader="1"]').forEach(function (el) {
|
|
114
|
+
el.remove();
|
|
115
|
+
});
|
|
116
|
+
window.__CRANK_LOADER__ = false;
|
|
117
|
+
appendLoaderScript(fallback);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
61
120
|
return undefined;
|
|
62
|
-
}, [
|
|
121
|
+
}, [preferredSrc]);
|
|
63
122
|
|
|
64
123
|
return null;
|
|
65
124
|
}
|
|
@@ -67,6 +126,7 @@ function Crank(props) {
|
|
|
67
126
|
module.exports = {
|
|
68
127
|
Crank,
|
|
69
128
|
CrankLoader: Crank,
|
|
129
|
+
detectReplitCompanionUrl,
|
|
70
130
|
resolveCrankCompanionUrl,
|
|
71
131
|
resolveCrankLoaderSrc,
|
|
72
132
|
default: Crank
|
package/react.mjs
CHANGED
|
@@ -1,20 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Browser-safe React entry — Vite-bundlable (no Node/fs).
|
|
3
3
|
*
|
|
4
|
-
* // main.tsx
|
|
5
4
|
* import { Crank } from '@ahmednawaz/crank/react'
|
|
6
|
-
* // or: import { Crank } from '@ahmednawaz/crank'
|
|
7
|
-
*
|
|
8
5
|
* <Crank />
|
|
9
6
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* Loads companion `/dev-loader.js` (prefers same-origin `/@crank` Vite proxy on Replit).
|
|
8
|
+
* Requires: `npx @ahmednawaz/crank start`
|
|
12
9
|
*
|
|
13
10
|
* peerDependency: react >= 17
|
|
14
11
|
*/
|
|
15
12
|
|
|
16
13
|
import { useEffect } from 'react';
|
|
17
14
|
|
|
15
|
+
const PROXY_LOADER = '/@crank/dev-loader.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Replit public companion host from the current preview hostname.
|
|
19
|
+
* e.g. 5000-foo.replit.dev → https://3344-foo.replit.dev
|
|
20
|
+
* @returns {string | null}
|
|
21
|
+
*/
|
|
22
|
+
export function detectReplitCompanionUrl() {
|
|
23
|
+
if (typeof window === 'undefined') {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const host = window.location.hostname || '';
|
|
28
|
+
if (!/\.replit\.dev$|\.repl\.co$|\.replit\.app$/.test(host)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const base = host.replace(/^\d+-/, '');
|
|
32
|
+
const port = 3344;
|
|
33
|
+
return `https://${port}-${base}`;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
18
39
|
/**
|
|
19
40
|
* @param {{ companionUrl?: string, url?: string, src?: string }} [props]
|
|
20
41
|
* @returns {string}
|
|
@@ -40,6 +61,10 @@ export function resolveCrankCompanionUrl(props = {}) {
|
|
|
40
61
|
} catch {
|
|
41
62
|
// ignore
|
|
42
63
|
}
|
|
64
|
+
const replit = detectReplitCompanionUrl();
|
|
65
|
+
if (replit) {
|
|
66
|
+
return replit;
|
|
67
|
+
}
|
|
43
68
|
return 'http://localhost:3344';
|
|
44
69
|
}
|
|
45
70
|
|
|
@@ -51,38 +76,67 @@ export function resolveCrankLoaderSrc(props = {}) {
|
|
|
51
76
|
if (props.src) {
|
|
52
77
|
return String(props.src);
|
|
53
78
|
}
|
|
79
|
+
// Same-origin Vite proxy first (Replit iframe / CSP safe).
|
|
80
|
+
if (typeof window !== 'undefined' && !props.companionUrl && !props.url && !window.__CRANK_URL__) {
|
|
81
|
+
try {
|
|
82
|
+
const envUrl =
|
|
83
|
+
typeof import.meta !== 'undefined' &&
|
|
84
|
+
import.meta.env &&
|
|
85
|
+
(import.meta.env.VITE_CRANK_URL || import.meta.env.VITE_CRANK_COMPANION_URL);
|
|
86
|
+
if (!envUrl) {
|
|
87
|
+
return PROXY_LOADER;
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
return PROXY_LOADER;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
54
93
|
return `${resolveCrankCompanionUrl(props)}/dev-loader.js`;
|
|
55
94
|
}
|
|
56
95
|
|
|
96
|
+
function appendLoaderScript(src, onError) {
|
|
97
|
+
const script = document.createElement('script');
|
|
98
|
+
script.src = src.includes('?') ? src : `${src}?t=${Date.now()}`;
|
|
99
|
+
script.async = true;
|
|
100
|
+
script.dataset.crankLoader = '1';
|
|
101
|
+
if (onError) {
|
|
102
|
+
script.onerror = onError;
|
|
103
|
+
}
|
|
104
|
+
document.documentElement.appendChild(script);
|
|
105
|
+
return script;
|
|
106
|
+
}
|
|
107
|
+
|
|
57
108
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* Do not set window.__CRANK_LOADER__ here — `/dev-loader.js` owns that flag and loads the panel.
|
|
109
|
+
* Mount near app root. Renders null; injects the companion loader once.
|
|
61
110
|
*
|
|
62
111
|
* @param {{ companionUrl?: string, url?: string, src?: string }} [props]
|
|
63
112
|
*/
|
|
64
113
|
export function Crank(props = {}) {
|
|
65
|
-
const
|
|
114
|
+
const preferredSrc = resolveCrankLoaderSrc(props);
|
|
66
115
|
|
|
67
116
|
useEffect(() => {
|
|
68
117
|
if (typeof document === 'undefined') {
|
|
69
118
|
return undefined;
|
|
70
119
|
}
|
|
71
|
-
if (document.
|
|
120
|
+
if (document.getElementById('vp-shell') || document.getElementById('vp-crank-reopen')) {
|
|
72
121
|
return undefined;
|
|
73
122
|
}
|
|
74
|
-
|
|
75
|
-
if (window.__CRANK_LOADER__ && document.getElementById('vp-shell')) {
|
|
123
|
+
if (document.querySelector('script[data-crank-loader="1"]')) {
|
|
76
124
|
return undefined;
|
|
77
125
|
}
|
|
78
126
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
127
|
+
const fallback = `${resolveCrankCompanionUrl(props)}/dev-loader.js`;
|
|
128
|
+
const primary = preferredSrc;
|
|
129
|
+
|
|
130
|
+
appendLoaderScript(primary, () => {
|
|
131
|
+
// Proxy missing (no Vite plugin) — fall back to public / localhost companion.
|
|
132
|
+
if (primary !== fallback && !document.getElementById('vp-shell')) {
|
|
133
|
+
document.querySelectorAll('script[data-crank-loader="1"]').forEach((el) => el.remove());
|
|
134
|
+
window.__CRANK_LOADER__ = false;
|
|
135
|
+
appendLoaderScript(fallback);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
84
138
|
return undefined;
|
|
85
|
-
}, [
|
|
139
|
+
}, [preferredSrc]);
|
|
86
140
|
|
|
87
141
|
return null;
|
|
88
142
|
}
|
package/vite.js
CHANGED
|
@@ -2,30 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Vite / Rolldown-vite / Vitest-compatible plugin.
|
|
5
|
-
* Injects Crank into the app during `vite` / `npm run dev` — no React component needed.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* // or: import { crank } from 'crank/vite'
|
|
10
|
-
* export default { plugins: [crank()] }
|
|
6
|
+
* - Proxies `/@crank/*` → companion (same-origin; required on Replit iframe previews)
|
|
7
|
+
* - Optionally injects the loader into index.html (skip when using React `<Crank />`)
|
|
11
8
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* import { crank } from '@ahmednawaz/crank/vite'
|
|
10
|
+
* export default { plugins: [crank()] } // Vite-only apps
|
|
11
|
+
* export default { plugins: [crank({ inject: false })] } // React + <Crank />
|
|
14
12
|
*/
|
|
15
13
|
|
|
16
|
-
const { resolveCompanionLoaderUrl } = require('./lib/loader-url');
|
|
14
|
+
const { resolveCompanionLoaderUrl, resolveCompanionProxyTarget } = require('./lib/loader-url');
|
|
15
|
+
|
|
16
|
+
const PROXY_PREFIX = '/@crank';
|
|
17
17
|
|
|
18
18
|
function crank(options = {}) {
|
|
19
|
-
const
|
|
19
|
+
const inject = options.inject !== false;
|
|
20
|
+
const target = resolveCompanionProxyTarget(options);
|
|
21
|
+
const src =
|
|
22
|
+
options.src ||
|
|
23
|
+
`${PROXY_PREFIX}/dev-loader.js`;
|
|
20
24
|
|
|
21
25
|
return {
|
|
22
26
|
name: 'crank-dev-loader',
|
|
23
27
|
apply: 'serve',
|
|
28
|
+
config() {
|
|
29
|
+
return {
|
|
30
|
+
server: {
|
|
31
|
+
proxy: {
|
|
32
|
+
[PROXY_PREFIX]: {
|
|
33
|
+
target,
|
|
34
|
+
changeOrigin: true,
|
|
35
|
+
ws: true,
|
|
36
|
+
rewrite: (p) => p.replace(new RegExp(`^${PROXY_PREFIX}`), '') || '/'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
},
|
|
24
42
|
transformIndexHtml(html) {
|
|
25
|
-
if (
|
|
43
|
+
if (!inject) {
|
|
44
|
+
return html;
|
|
45
|
+
}
|
|
46
|
+
if (
|
|
47
|
+
String(html).includes('/dev-loader.js') ||
|
|
48
|
+
String(html).includes('crank/dev-loader') ||
|
|
49
|
+
String(html).includes('/@crank/')
|
|
50
|
+
) {
|
|
26
51
|
return html;
|
|
27
52
|
}
|
|
28
|
-
const tag = `<script src="${src}" defer></script>`;
|
|
53
|
+
const tag = `<script src="${src}?t=${Date.now()}" defer></script>`;
|
|
29
54
|
if (html.includes('</head>')) {
|
|
30
55
|
return html.replace('</head>', ` ${tag}\n</head>`);
|
|
31
56
|
}
|
|
@@ -37,5 +62,6 @@ function crank(options = {}) {
|
|
|
37
62
|
module.exports = {
|
|
38
63
|
crank,
|
|
39
64
|
crankDevLoader: crank,
|
|
40
|
-
resolveCompanionLoaderUrl
|
|
65
|
+
resolveCompanionLoaderUrl,
|
|
66
|
+
PROXY_PREFIX
|
|
41
67
|
};
|