@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 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
- - If preview is not localhost: `window.__CRANK_URL__` or `VITE_CRANK_URL` = public `https://3344-…` URL
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
- return new URL(s.src, location.href).origin;
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
- if (/\bcrank\s*\?\s*\[\s*crank\s*\(\s*\)\s*\]/.test(source) || /\[\s*\.\.\.\s*\(\s*crank\s*\?/.test(source)) {
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*\]/, 'plugins: [...(crank ? [crank()] : [])]'),
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*\[/, 'plugins: [...(crank ? [crank()] : []), '),
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
- 'defineConfig({\n plugins: [...(crank ? [crank()] : [])],'
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
- 'export default {\n plugins: [...(crank ? [crank()] : [])],'
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
- 'module.exports = {\n plugins: [...(crank ? [crank()] : [])],'
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
- if (env.hasVite) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahmednawaz/crank",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Internal Motive UX copy iteration: select UI text \u2192 Glean variants \u2192 MCP apply. Cursor, Replit, Claude.",
5
5
  "repository": {
6
6
  "type": "git",
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
- * Retune-style mount — injects companion `/dev-loader.js` once. Renders null.
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` for Replit public companion host.
22
- * Keep companion running: `npx @ahmednawaz/crank start`
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
- return `${resolveCrankCompanionUrl(props)}/dev-loader.js`;
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 src = resolveCrankLoaderSrc(props);
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.querySelector('script[data-crank-loader="1"]')) {
103
+ if (document.getElementById('vp-shell') || document.getElementById('vp-crank-reopen')) {
50
104
  return undefined;
51
105
  }
52
- if (window.__CRANK_LOADER__ && document.getElementById('vp-shell')) {
106
+ if (document.querySelector('script[data-crank-loader="1"]')) {
53
107
  return undefined;
54
108
  }
55
109
 
56
- const script = document.createElement('script');
57
- script.src = src.indexOf('?') >= 0 ? src : src + '?t=' + Date.now();
58
- script.async = true;
59
- script.dataset.crankLoader = '1';
60
- document.documentElement.appendChild(script);
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
- }, [src]);
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
- * Retune-style React entry — browser-only, Vite-bundlable (no Node/fs).
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
- * Mount near your app root. Injects companion `/dev-loader.js` once (same panel as the
11
- * optional Vite plugin). Requires the companion process: `npx @ahmednawaz/crank start`
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
- * Retune-style one-liner. Renders null; injects the companion loader script on the client.
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 src = resolveCrankLoaderSrc(props);
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.querySelector('script[data-crank-loader="1"]')) {
120
+ if (document.getElementById('vp-shell') || document.getElementById('vp-crank-reopen')) {
72
121
  return undefined;
73
122
  }
74
- // Panel already live (bookmarklet / prior mount)
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 script = document.createElement('script');
80
- script.src = src.includes('?') ? src : `${src}?t=${Date.now()}`;
81
- script.async = true;
82
- script.dataset.crankLoader = '1';
83
- document.documentElement.appendChild(script);
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
- }, [src]);
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
- * // vite.config.ts
8
- * import { crank } from '@ahmednawaz/crank/vite'
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
- * Prefer this for Vite apps (React, Vue, Svelte, vanilla). The panel is a script overlay,
13
- * not a component in your React tree.
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 src = resolveCompanionLoaderUrl(options);
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 (String(html).includes('/dev-loader.js') || String(html).includes('crank/dev-loader')) {
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
  };