@ahmednawaz/crank 0.1.8 → 0.1.9
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/AGENTS.md +17 -0
- package/README.md +14 -0
- package/lib/detect.js +128 -28
- package/lib/init.js +317 -43
- package/lib/loader-url.js +55 -0
- package/next.js +63 -0
- package/package.json +17 -1
- package/react.js +69 -0
- package/skill/SKILL.md +1 -0
- package/vite.js +7 -41
- package/webpack.js +58 -0
package/AGENTS.md
CHANGED
|
@@ -103,6 +103,23 @@ See **[REPLIT.md](./REPLIT.md)** and `.crank/replit.md` (URLs after first run).
|
|
|
103
103
|
|
|
104
104
|
---
|
|
105
105
|
|
|
106
|
+
## Panel injection (Vite vs React)
|
|
107
|
+
|
|
108
|
+
Crank’s panel is a **script overlay** (`/dev-loader.js` → bookmarklet), **not** a Retune-style React component in the app tree.
|
|
109
|
+
|
|
110
|
+
| Stack | What to do |
|
|
111
|
+
|-------|------------|
|
|
112
|
+
| **Vite** (React/Vue/Svelte/vanilla) | **Prefer** Vite plugin. `setup` writes `.crank/vite-plugin.cjs` and patches `vite.config` with Vizpatch-style graceful load (works when `pnpm add` fails — uses `packageRoot` from `.crank/config.json`). |
|
|
113
|
+
| **Next.js** | Optional: `import { CrankScript } from '@ahmednawaz/crank/next'` |
|
|
114
|
+
| **React without Vite** | Optional: `import { CrankLoader } from '@ahmednawaz/crank/react'` — injects the same script only |
|
|
115
|
+
| **Any** | Bookmarklet from companion URL |
|
|
116
|
+
|
|
117
|
+
**Agents must not** invent a fake in-tree `<Crank />` panel UI. If `hasVite: false` but a `vite.config.*` exists nearby, re-run setup from the app root or check `environment.viteConfig` from `doctor`.
|
|
118
|
+
|
|
119
|
+
Keep companion up via Workflow: `npx @ahmednawaz/crank start`. See `.crank/injection.md` after setup.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
106
123
|
## MCP tools (agent pull workflow)
|
|
107
124
|
|
|
108
125
|
When the user clicks **Agent** on a copy variant or says *“Apply the pending Crank change”*:
|
package/README.md
CHANGED
|
@@ -102,6 +102,20 @@ No external sibling tool required. Crank ships:
|
|
|
102
102
|
|
|
103
103
|
Optional: `CRANK_ASSET_ROOT` overrides where packaged UI assets are loaded from (defaults to this install).
|
|
104
104
|
|
|
105
|
+
### Panel injection (Vite first)
|
|
106
|
+
|
|
107
|
+
The panel is a **script overlay**, not a Retune-style React component.
|
|
108
|
+
|
|
109
|
+
| Stack | Integration |
|
|
110
|
+
|-------|-------------|
|
|
111
|
+
| **Vite** | `setup` patches `vite.config` with Vizpatch-style graceful load via `.crank/vite-plugin.cjs` (resolves from `packageRoot` / npx cache when `pnpm add` fails) |
|
|
112
|
+
| **Next.js** | `import { CrankScript } from '@ahmednawaz/crank/next'` |
|
|
113
|
+
| **React (no Vite)** | `import { CrankLoader } from '@ahmednawaz/crank/react'` — script injector only |
|
|
114
|
+
| **Webpack** | `crankWebpackPlugin()` from `@ahmednawaz/crank/webpack` |
|
|
115
|
+
| **Fallback** | Companion bookmarklet |
|
|
116
|
+
|
|
117
|
+
Keep companion running (`npx @ahmednawaz/crank start`). See `.crank/injection.md` after setup.
|
|
118
|
+
|
|
105
119
|
Useful variants:
|
|
106
120
|
|
|
107
121
|
```bash
|
package/lib/detect.js
CHANGED
|
@@ -4,6 +4,41 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
|
|
7
|
+
const VITE_CONFIG_NAMES = [
|
|
8
|
+
'vite.config.ts',
|
|
9
|
+
'vite.config.js',
|
|
10
|
+
'vite.config.mjs',
|
|
11
|
+
'vite.config.cjs',
|
|
12
|
+
'vite.config.mts',
|
|
13
|
+
'vite.config.cts'
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const APP_DIR_CANDIDATES = [
|
|
17
|
+
'.',
|
|
18
|
+
'app',
|
|
19
|
+
'client',
|
|
20
|
+
'web',
|
|
21
|
+
'frontend',
|
|
22
|
+
'ui',
|
|
23
|
+
'src',
|
|
24
|
+
'packages/app',
|
|
25
|
+
'packages/web',
|
|
26
|
+
'apps/web',
|
|
27
|
+
'apps/frontend'
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const SKIP_DIRS = new Set([
|
|
31
|
+
'node_modules',
|
|
32
|
+
'.git',
|
|
33
|
+
'dist',
|
|
34
|
+
'build',
|
|
35
|
+
'.next',
|
|
36
|
+
'coverage',
|
|
37
|
+
'.turbo',
|
|
38
|
+
'.cache',
|
|
39
|
+
'.crank'
|
|
40
|
+
]);
|
|
41
|
+
|
|
7
42
|
function exists(p) {
|
|
8
43
|
try {
|
|
9
44
|
return fs.existsSync(p);
|
|
@@ -25,21 +60,97 @@ function writeJson(file, data) {
|
|
|
25
60
|
fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`);
|
|
26
61
|
}
|
|
27
62
|
|
|
63
|
+
function findViteConfigInDir(dir) {
|
|
64
|
+
for (const name of VITE_CONFIG_NAMES) {
|
|
65
|
+
const full = path.join(dir, name);
|
|
66
|
+
if (exists(full)) {
|
|
67
|
+
return full;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Find vite.config.* at project root, common app subdirs, then shallow BFS.
|
|
75
|
+
*/
|
|
76
|
+
function findViteConfig(projectRoot) {
|
|
77
|
+
const root = path.resolve(projectRoot);
|
|
78
|
+
for (const sub of APP_DIR_CANDIDATES) {
|
|
79
|
+
const dir = sub === '.' ? root : path.join(root, sub);
|
|
80
|
+
const hit = findViteConfigInDir(dir);
|
|
81
|
+
if (hit) {
|
|
82
|
+
return hit;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Shallow walk (depth 2) for monorepos / Replit layouts
|
|
87
|
+
const queue = [{ dir: root, depth: 0 }];
|
|
88
|
+
while (queue.length) {
|
|
89
|
+
const { dir, depth } = queue.shift();
|
|
90
|
+
if (depth > 0) {
|
|
91
|
+
const hit = findViteConfigInDir(dir);
|
|
92
|
+
if (hit) {
|
|
93
|
+
return hit;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (depth >= 2) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
let entries;
|
|
100
|
+
try {
|
|
101
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
102
|
+
} catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function packageHasDep(pkg, name) {
|
|
116
|
+
if (!pkg || typeof pkg !== 'object') {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return Boolean(
|
|
120
|
+
pkg.dependencies?.[name] ||
|
|
121
|
+
pkg.devDependencies?.[name] ||
|
|
122
|
+
pkg.peerDependencies?.[name]
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
28
126
|
function detectEnvironment(projectRoot) {
|
|
29
127
|
const env = process.env;
|
|
30
128
|
const isReplit = Boolean(
|
|
31
129
|
env.REPL_ID || env.REPL_SLUG || env.REPLIT_DEV_DOMAIN || env.REPLIT_DB_URL
|
|
32
130
|
);
|
|
33
|
-
const hasVite =
|
|
34
|
-
exists(path.join(projectRoot, 'vite.config.ts')) ||
|
|
35
|
-
exists(path.join(projectRoot, 'vite.config.js')) ||
|
|
36
|
-
exists(path.join(projectRoot, 'vite.config.mjs')) ||
|
|
37
|
-
exists(path.join(projectRoot, 'vite.config.cjs'));
|
|
38
131
|
const hasPackageJson = exists(path.join(projectRoot, 'package.json'));
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
132
|
+
const packageJson = hasPackageJson
|
|
133
|
+
? readJson(path.join(projectRoot, 'package.json'), {})
|
|
134
|
+
: null;
|
|
135
|
+
const viteConfig = findViteConfig(projectRoot);
|
|
136
|
+
const hasVite = Boolean(viteConfig) || packageHasDep(packageJson, 'vite');
|
|
137
|
+
const hasReact =
|
|
138
|
+
packageHasDep(packageJson, 'react') ||
|
|
139
|
+
exists(path.join(projectRoot, 'next.config.js')) ||
|
|
140
|
+
exists(path.join(projectRoot, 'next.config.mjs')) ||
|
|
141
|
+
exists(path.join(projectRoot, 'next.config.ts'));
|
|
142
|
+
const hasNext =
|
|
143
|
+
packageHasDep(packageJson, 'next') ||
|
|
144
|
+
exists(path.join(projectRoot, 'next.config.js')) ||
|
|
145
|
+
exists(path.join(projectRoot, 'next.config.mjs')) ||
|
|
146
|
+
exists(path.join(projectRoot, 'next.config.ts'));
|
|
147
|
+
const hasWebpack =
|
|
148
|
+
packageHasDep(packageJson, 'webpack') ||
|
|
149
|
+
exists(path.join(projectRoot, 'webpack.config.js')) ||
|
|
150
|
+
exists(path.join(projectRoot, 'webpack.config.ts'));
|
|
151
|
+
|
|
152
|
+
const hasCursor =
|
|
153
|
+
exists(path.join(projectRoot, '.cursor')) || exists(path.join(os.homedir(), '.cursor'));
|
|
43
154
|
const claudeConfig = path.join(
|
|
44
155
|
os.homedir(),
|
|
45
156
|
'Library',
|
|
@@ -52,37 +163,26 @@ function detectEnvironment(projectRoot) {
|
|
|
52
163
|
return {
|
|
53
164
|
isReplit,
|
|
54
165
|
hasVite,
|
|
166
|
+
viteConfig,
|
|
167
|
+
hasReact,
|
|
168
|
+
hasNext,
|
|
169
|
+
hasWebpack,
|
|
55
170
|
hasPackageJson,
|
|
56
171
|
hasCursor,
|
|
57
172
|
hasVsCode: exists(path.join(projectRoot, '.vscode')),
|
|
58
173
|
hasClaudeDesktop,
|
|
59
174
|
claudeConfig,
|
|
60
175
|
replitDomain: env.REPLIT_DEV_DOMAIN || null,
|
|
61
|
-
packageJson
|
|
62
|
-
? readJson(path.join(projectRoot, 'package.json'), {})
|
|
63
|
-
: null
|
|
176
|
+
packageJson
|
|
64
177
|
};
|
|
65
178
|
}
|
|
66
179
|
|
|
67
|
-
function findViteConfig(projectRoot) {
|
|
68
|
-
for (const name of [
|
|
69
|
-
'vite.config.ts',
|
|
70
|
-
'vite.config.js',
|
|
71
|
-
'vite.config.mjs',
|
|
72
|
-
'vite.config.cjs'
|
|
73
|
-
]) {
|
|
74
|
-
const full = path.join(projectRoot, name);
|
|
75
|
-
if (exists(full)) {
|
|
76
|
-
return full;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
180
|
module.exports = {
|
|
83
181
|
exists,
|
|
84
182
|
readJson,
|
|
85
183
|
writeJson,
|
|
86
184
|
detectEnvironment,
|
|
87
|
-
findViteConfig
|
|
185
|
+
findViteConfig,
|
|
186
|
+
VITE_CONFIG_NAMES,
|
|
187
|
+
APP_DIR_CANDIDATES
|
|
88
188
|
};
|
package/lib/init.js
CHANGED
|
@@ -94,59 +94,214 @@ function mergeVsCodeMcp(file, projectRoot) {
|
|
|
94
94
|
return file;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
function writeVitePluginStub(projectRoot) {
|
|
98
|
+
const dir = path.join(projectRoot, '.crank');
|
|
99
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
100
|
+
const stubPath = path.join(dir, 'vite-plugin.cjs');
|
|
101
|
+
const stub = `'use strict';
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Crank Vite plugin loader — Vizpatch-style graceful resolve.
|
|
105
|
+
* Generated by \`npx @ahmednawaz/crank setup\`. Does not throw if Crank is missing.
|
|
106
|
+
*
|
|
107
|
+
* Resolves from (in order):
|
|
108
|
+
* 1. .crank/config.json packageRoot (npx cache / local checkout)
|
|
109
|
+
* 2. node_modules/@ahmednawaz/crank or node_modules/crank
|
|
110
|
+
* 3. require('@ahmednawaz/crank/vite')
|
|
111
|
+
*/
|
|
112
|
+
const fs = require('fs');
|
|
113
|
+
const path = require('path');
|
|
114
|
+
|
|
115
|
+
function load() {
|
|
116
|
+
const candidates = [];
|
|
117
|
+
try {
|
|
118
|
+
const configPath = path.join(__dirname, 'config.json');
|
|
119
|
+
if (fs.existsSync(configPath)) {
|
|
120
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
121
|
+
if (cfg && cfg.packageRoot) {
|
|
122
|
+
candidates.push(path.join(cfg.packageRoot, 'vite.js'));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
// ignore
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const cwd = process.cwd();
|
|
130
|
+
candidates.push(
|
|
131
|
+
path.join(cwd, 'node_modules', '@ahmednawaz', 'crank', 'vite.js'),
|
|
132
|
+
path.join(cwd, 'node_modules', 'crank', 'vite.js'),
|
|
133
|
+
path.join(__dirname, '..', 'node_modules', '@ahmednawaz', 'crank', 'vite.js'),
|
|
134
|
+
path.join(__dirname, '..', 'node_modules', 'crank', 'vite.js')
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
for (const file of candidates) {
|
|
138
|
+
try {
|
|
139
|
+
if (file && fs.existsSync(file)) {
|
|
140
|
+
return require(file);
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// try next
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
return require('@ahmednawaz/crank/vite');
|
|
149
|
+
} catch {
|
|
150
|
+
// ignore
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
return require('@ahmednawaz/crank');
|
|
154
|
+
} catch {
|
|
155
|
+
// ignore
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
return require('crank/vite');
|
|
159
|
+
} catch {
|
|
160
|
+
// ignore
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function noopPlugin() {
|
|
166
|
+
return {
|
|
167
|
+
name: 'crank-dev-loader-noop',
|
|
168
|
+
apply: 'serve',
|
|
169
|
+
transformIndexHtml(html) {
|
|
170
|
+
return html;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const mod = load();
|
|
176
|
+
const crankFn =
|
|
177
|
+
mod && typeof mod.crank === 'function' ? mod.crank : noopPlugin;
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
crank: crankFn,
|
|
181
|
+
crankDevLoader: crankFn,
|
|
182
|
+
resolveCompanionLoaderUrl: mod && mod.resolveCompanionLoaderUrl
|
|
183
|
+
};
|
|
184
|
+
`;
|
|
185
|
+
fs.writeFileSync(stubPath, stub);
|
|
186
|
+
return stubPath;
|
|
187
|
+
}
|
|
188
|
+
|
|
97
189
|
function patchViteConfig(projectRoot) {
|
|
98
190
|
const configPath = findViteConfig(projectRoot);
|
|
191
|
+
const stubPath = writeVitePluginStub(projectRoot);
|
|
192
|
+
|
|
99
193
|
if (!configPath) {
|
|
100
|
-
return { patched: false, reason: 'no-vite-config' };
|
|
194
|
+
return { patched: false, reason: 'no-vite-config', plugin: stubPath };
|
|
101
195
|
}
|
|
196
|
+
|
|
102
197
|
let source = fs.readFileSync(configPath, 'utf8');
|
|
103
|
-
if (
|
|
104
|
-
|
|
198
|
+
if (
|
|
199
|
+
/vite-plugin\.cjs/.test(source) ||
|
|
200
|
+
(/(\bcrank)\s*\(/.test(source) && /plugins/.test(source))
|
|
201
|
+
) {
|
|
202
|
+
return { patched: false, reason: 'already-present', file: configPath, plugin: stubPath };
|
|
105
203
|
}
|
|
106
204
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'vite.js'), localPlugin);
|
|
205
|
+
let relStub = path.relative(path.dirname(configPath), stubPath).replace(/\\/g, '/');
|
|
206
|
+
if (!relStub.startsWith('.')) {
|
|
207
|
+
relStub = `./${relStub}`;
|
|
208
|
+
}
|
|
112
209
|
|
|
113
|
-
const rel = './.crank/vite-plugin.cjs';
|
|
114
210
|
const isEsm =
|
|
115
211
|
configPath.endsWith('.mjs') ||
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
)
|
|
212
|
+
configPath.endsWith('.mts') ||
|
|
213
|
+
(/^import\s/m.test(source) && !configPath.endsWith('.cjs') && !configPath.endsWith('.cts'));
|
|
214
|
+
|
|
215
|
+
if (isEsm) {
|
|
216
|
+
const isTs = /\.tsx?$|\.mts$/.test(configPath);
|
|
217
|
+
const crankDecl = isTs
|
|
218
|
+
? 'let crank: null | (() => unknown) = null'
|
|
219
|
+
: 'let crank = null';
|
|
220
|
+
const gracefulBlock = `import { createRequire } from 'node:module'
|
|
221
|
+
import { existsSync } from 'node:fs'
|
|
222
|
+
import { fileURLToPath } from 'node:url'
|
|
223
|
+
|
|
224
|
+
// Crank Vite plugin — graceful load (same pattern as Vizpatch); skip if unavailable
|
|
225
|
+
${crankDecl}
|
|
226
|
+
try {
|
|
227
|
+
const _crankPluginPath = fileURLToPath(new URL('${relStub}', import.meta.url))
|
|
228
|
+
if (existsSync(_crankPluginPath)) {
|
|
229
|
+
const require = createRequire(import.meta.url)
|
|
230
|
+
crank = require(_crankPluginPath).crank
|
|
231
|
+
}
|
|
232
|
+
} catch {}
|
|
233
|
+
|
|
234
|
+
`;
|
|
235
|
+
if (!/_crankPluginPath/.test(source)) {
|
|
236
|
+
source = gracefulBlock + source;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (/plugins\s*:\s*\[\s*\]/.test(source)) {
|
|
240
|
+
source = source.replace(
|
|
241
|
+
/plugins\s*:\s*\[\s*\]/,
|
|
242
|
+
'plugins: [...(crank ? [crank()] : [])]'
|
|
243
|
+
);
|
|
244
|
+
} else if (/plugins\s*:\s*\[/.test(source)) {
|
|
245
|
+
source = source.replace(
|
|
246
|
+
/plugins\s*:\s*\[/,
|
|
247
|
+
'plugins: [...(crank ? [crank()] : []), '
|
|
248
|
+
);
|
|
249
|
+
} else if (/defineConfig\s*\(\s*\{/.test(source)) {
|
|
250
|
+
source = source.replace(
|
|
251
|
+
/defineConfig\s*\(\s*\{/,
|
|
252
|
+
'defineConfig({\n plugins: [...(crank ? [crank()] : [])],'
|
|
253
|
+
);
|
|
254
|
+
} else if (/export\s+default\s+\{/.test(source)) {
|
|
255
|
+
source = source.replace(
|
|
256
|
+
/export\s+default\s+\{/,
|
|
257
|
+
'export default {\n plugins: [...(crank ? [crank()] : [])],'
|
|
258
|
+
);
|
|
259
|
+
} else {
|
|
260
|
+
return {
|
|
261
|
+
patched: false,
|
|
262
|
+
reason: 'unrecognized-vite-shape',
|
|
263
|
+
file: configPath,
|
|
264
|
+
plugin: stubPath
|
|
265
|
+
};
|
|
266
|
+
}
|
|
144
267
|
} else {
|
|
145
|
-
|
|
268
|
+
const gracefulBlock = `// Crank Vite plugin — graceful load (same pattern as Vizpatch)
|
|
269
|
+
let crank = null;
|
|
270
|
+
try {
|
|
271
|
+
crank = require('${relStub}').crank;
|
|
272
|
+
} catch {}
|
|
273
|
+
|
|
274
|
+
`;
|
|
275
|
+
if (!/vite-plugin\.cjs/.test(source)) {
|
|
276
|
+
source = gracefulBlock + source;
|
|
277
|
+
}
|
|
278
|
+
if (/plugins\s*:\s*\[\s*\]/.test(source)) {
|
|
279
|
+
source = source.replace(
|
|
280
|
+
/plugins\s*:\s*\[\s*\]/,
|
|
281
|
+
'plugins: [...(crank ? [crank()] : [])]'
|
|
282
|
+
);
|
|
283
|
+
} else if (/plugins\s*:\s*\[/.test(source)) {
|
|
284
|
+
source = source.replace(
|
|
285
|
+
/plugins\s*:\s*\[/,
|
|
286
|
+
'plugins: [...(crank ? [crank()] : []), '
|
|
287
|
+
);
|
|
288
|
+
} else if (/module\.exports\s*=\s*\{/.test(source)) {
|
|
289
|
+
source = source.replace(
|
|
290
|
+
/module\.exports\s*=\s*\{/,
|
|
291
|
+
'module.exports = {\n plugins: [...(crank ? [crank()] : [])],'
|
|
292
|
+
);
|
|
293
|
+
} else {
|
|
294
|
+
return {
|
|
295
|
+
patched: false,
|
|
296
|
+
reason: 'unrecognized-vite-shape',
|
|
297
|
+
file: configPath,
|
|
298
|
+
plugin: stubPath
|
|
299
|
+
};
|
|
300
|
+
}
|
|
146
301
|
}
|
|
147
302
|
|
|
148
303
|
fs.writeFileSync(configPath, source);
|
|
149
|
-
return { patched: true, file: configPath, plugin:
|
|
304
|
+
return { patched: true, file: configPath, plugin: stubPath };
|
|
150
305
|
}
|
|
151
306
|
|
|
152
307
|
function writeProjectEnvExample(projectRoot) {
|
|
@@ -327,7 +482,11 @@ function writeConfig(projectRoot, env) {
|
|
|
327
482
|
createdAt: new Date().toISOString(),
|
|
328
483
|
environment: {
|
|
329
484
|
isReplit: env.isReplit,
|
|
330
|
-
hasVite: env.hasVite
|
|
485
|
+
hasVite: env.hasVite,
|
|
486
|
+
viteConfig: env.viteConfig || null,
|
|
487
|
+
hasReact: env.hasReact,
|
|
488
|
+
hasNext: env.hasNext,
|
|
489
|
+
hasWebpack: env.hasWebpack
|
|
331
490
|
}
|
|
332
491
|
};
|
|
333
492
|
writeJson(configPath, config);
|
|
@@ -376,8 +535,18 @@ function initProject(projectRoot, options = {}) {
|
|
|
376
535
|
if (viteResult.patched) {
|
|
377
536
|
results.wrote.push(path.relative(projectRoot, viteResult.file));
|
|
378
537
|
}
|
|
538
|
+
if (viteResult.plugin) {
|
|
539
|
+
results.wrote.push(path.relative(projectRoot, viteResult.plugin));
|
|
540
|
+
}
|
|
541
|
+
} else if (options.vite !== false) {
|
|
542
|
+
// Still write stub so agents can wire the graceful load manually.
|
|
543
|
+
const stub = writeVitePluginStub(projectRoot);
|
|
544
|
+
results.vite = { patched: false, reason: 'no-vite-detected', plugin: stub };
|
|
545
|
+
results.wrote.push(path.relative(projectRoot, stub));
|
|
379
546
|
}
|
|
380
547
|
|
|
548
|
+
writeInjectionGuide(projectRoot, env, results);
|
|
549
|
+
|
|
381
550
|
// package.json script convenience
|
|
382
551
|
if (env.hasPackageJson && options.scripts !== false) {
|
|
383
552
|
const pkgPath = path.join(projectRoot, 'package.json');
|
|
@@ -530,6 +699,94 @@ function copyAgentsDoc(projectRoot, results) {
|
|
|
530
699
|
}
|
|
531
700
|
}
|
|
532
701
|
|
|
702
|
+
function writeInjectionGuide(projectRoot, env, results) {
|
|
703
|
+
const guide = path.join(projectRoot, '.crank', 'injection.md');
|
|
704
|
+
fs.mkdirSync(path.dirname(guide), { recursive: true });
|
|
705
|
+
const lines = [
|
|
706
|
+
'# Crank panel injection',
|
|
707
|
+
'',
|
|
708
|
+
'Crank’s panel is a **script overlay** (companion `/dev-loader.js` → bookmarklet).',
|
|
709
|
+
'It is **not** a Retune-style React UI component living in your component tree.',
|
|
710
|
+
'',
|
|
711
|
+
'## Prefer Vite plugin (default)',
|
|
712
|
+
'',
|
|
713
|
+
'For Vite apps (React, Vue, Svelte, vanilla):',
|
|
714
|
+
'',
|
|
715
|
+
'1. Run `npx @ahmednawaz/crank setup` (writes `.crank/vite-plugin.cjs` + patches `vite.config`).',
|
|
716
|
+
'2. Keep a **Workflow** running: `npx @ahmednawaz/crank start` (companion on **3344**).',
|
|
717
|
+
'3. Dev server loads the panel automatically via the Vite plugin.',
|
|
718
|
+
'',
|
|
719
|
+
'Graceful load (Vizpatch-style) — works even when `pnpm add @ahmednawaz/crank` fails:',
|
|
720
|
+
'',
|
|
721
|
+
'```ts',
|
|
722
|
+
"import { createRequire } from 'node:module'",
|
|
723
|
+
"import { existsSync } from 'node:fs'",
|
|
724
|
+
"import { fileURLToPath } from 'node:url'",
|
|
725
|
+
'',
|
|
726
|
+
'let crank = null',
|
|
727
|
+
'try {',
|
|
728
|
+
" const pluginPath = fileURLToPath(new URL('./.crank/vite-plugin.cjs', import.meta.url))",
|
|
729
|
+
' if (existsSync(pluginPath)) {',
|
|
730
|
+
' const require = createRequire(import.meta.url)',
|
|
731
|
+
' crank = require(pluginPath).crank',
|
|
732
|
+
' }',
|
|
733
|
+
'} catch {}',
|
|
734
|
+
'',
|
|
735
|
+
'export default defineConfig({',
|
|
736
|
+
' plugins: [',
|
|
737
|
+
' ...(crank ? [crank()] : []),',
|
|
738
|
+
' // ...your plugins',
|
|
739
|
+
' ],',
|
|
740
|
+
'})',
|
|
741
|
+
'```',
|
|
742
|
+
'',
|
|
743
|
+
'`.crank/vite-plugin.cjs` resolves Crank from `config.json` → `packageRoot` (npx cache),',
|
|
744
|
+
'then `node_modules`, and noops if missing.',
|
|
745
|
+
'',
|
|
746
|
+
'## React adapter (non-Vite React only)',
|
|
747
|
+
'',
|
|
748
|
+
'Use only when there is **no** Vite config (CRA / custom webpack React):',
|
|
749
|
+
'',
|
|
750
|
+
'```tsx',
|
|
751
|
+
"import { CrankLoader } from '@ahmednawaz/crank/react'",
|
|
752
|
+
'// client root only — injects the same /dev-loader.js script',
|
|
753
|
+
'<CrankLoader />',
|
|
754
|
+
'```',
|
|
755
|
+
'',
|
|
756
|
+
'Do **not** invent a fake in-tree `<Crank />` panel UI. `CrankLoader` / `Crank` only inject a script.',
|
|
757
|
+
'',
|
|
758
|
+
'## Next.js',
|
|
759
|
+
'',
|
|
760
|
+
'```tsx',
|
|
761
|
+
"import { CrankScript } from '@ahmednawaz/crank/next'",
|
|
762
|
+
'// app/layout.tsx or pages/_app',
|
|
763
|
+
'<CrankScript />',
|
|
764
|
+
'```',
|
|
765
|
+
'',
|
|
766
|
+
'## Webpack',
|
|
767
|
+
'',
|
|
768
|
+
'```js',
|
|
769
|
+
"const { crankWebpackPlugin } = require('@ahmednawaz/crank/webpack')",
|
|
770
|
+
'plugins: [new HtmlWebpackPlugin(...), crankWebpackPlugin()]',
|
|
771
|
+
'```',
|
|
772
|
+
'',
|
|
773
|
+
'## Bookmarklet fallback',
|
|
774
|
+
'',
|
|
775
|
+
'Open the companion URL → drag the bookmarklet onto any preview page.',
|
|
776
|
+
'',
|
|
777
|
+
'## Detected in this project',
|
|
778
|
+
'',
|
|
779
|
+
`- hasVite: ${Boolean(env.hasVite)}`,
|
|
780
|
+
`- viteConfig: ${env.viteConfig || '(none)'}`,
|
|
781
|
+
`- hasReact: ${Boolean(env.hasReact)}`,
|
|
782
|
+
`- hasNext: ${Boolean(env.hasNext)}`,
|
|
783
|
+
`- hasWebpack: ${Boolean(env.hasWebpack)}`,
|
|
784
|
+
''
|
|
785
|
+
];
|
|
786
|
+
fs.writeFileSync(guide, lines.join('\n'));
|
|
787
|
+
results.wrote.push('.crank/injection.md');
|
|
788
|
+
}
|
|
789
|
+
|
|
533
790
|
function printSetupNextSteps(env, runtime, results) {
|
|
534
791
|
console.log('');
|
|
535
792
|
console.log(' Setup complete (Retune-style):');
|
|
@@ -539,6 +796,16 @@ function printSetupNextSteps(env, runtime, results) {
|
|
|
539
796
|
if (results?.wrote?.some((w) => String(w).includes('skills'))) {
|
|
540
797
|
console.log(' • Skill crank-apply-changes installed (auto-syncs on start)');
|
|
541
798
|
}
|
|
799
|
+
if (results?.vite?.patched) {
|
|
800
|
+
console.log(' • Vite plugin patched (graceful load via .crank/vite-plugin.cjs)');
|
|
801
|
+
} else if (env.hasVite) {
|
|
802
|
+
console.log(' • Vite detected — see .crank/injection.md if the panel does not auto-load');
|
|
803
|
+
} else if (env.hasNext) {
|
|
804
|
+
console.log(' • Next.js — add <CrankScript /> from @ahmednawaz/crank/next (see .crank/injection.md)');
|
|
805
|
+
} else if (env.hasReact) {
|
|
806
|
+
console.log(' • React without Vite — add <CrankLoader /> from @ahmednawaz/crank/react (see .crank/injection.md)');
|
|
807
|
+
}
|
|
808
|
+
console.log(' • Injection guide: .crank/injection.md');
|
|
542
809
|
if (env.isReplit) {
|
|
543
810
|
console.log(' • Workflow command: npx @ahmednawaz/crank start');
|
|
544
811
|
console.log(' • Add CRANK_TEAM_TOKEN in Secrets (not in the command)');
|
|
@@ -591,8 +858,10 @@ function writeReplitGuide(projectRoot, detectEnv, results, runtime) {
|
|
|
591
858
|
'## Panel in preview',
|
|
592
859
|
'',
|
|
593
860
|
`- Companion: ${companionUrl}`,
|
|
594
|
-
'- Vite
|
|
595
|
-
'-
|
|
861
|
+
'- **Vite:** panel auto-loads via `.crank/vite-plugin.cjs` (graceful load). Keep companion Workflow running.',
|
|
862
|
+
'- **React/Next without Vite:** see `.crank/injection.md` (`CrankLoader` / `CrankScript`).',
|
|
863
|
+
'- **Fallback:** open the companion URL and use the bookmarklet on your preview tab.',
|
|
864
|
+
'- Full guide: `.crank/injection.md`',
|
|
596
865
|
''
|
|
597
866
|
].join('\n')
|
|
598
867
|
);
|
|
@@ -608,5 +877,10 @@ module.exports = {
|
|
|
608
877
|
configureMcpRetuneStyle,
|
|
609
878
|
printSetupNextSteps,
|
|
610
879
|
writeReplitGuide,
|
|
611
|
-
|
|
880
|
+
writeReplitPlatformGuide,
|
|
881
|
+
writeVitePluginStub,
|
|
882
|
+
patchViteConfig,
|
|
883
|
+
writeInjectionGuide,
|
|
884
|
+
patchReplitConfig,
|
|
885
|
+
copyAgentsDoc
|
|
612
886
|
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared companion loader URL for Vite / React / Next / webpack adapters.
|
|
5
|
+
* All paths inject the same /dev-loader.js → bookmarklet panel (not a React UI tree).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { env } = require('./env');
|
|
11
|
+
const { readRuntimeManifest } = require('./urls');
|
|
12
|
+
|
|
13
|
+
function findProjectRoot(startDir) {
|
|
14
|
+
let dir = path.resolve(startDir || process.cwd());
|
|
15
|
+
for (let i = 0; i < 8; i++) {
|
|
16
|
+
if (fs.existsSync(path.join(dir, '.crank', 'runtime.json'))) {
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
const parent = path.dirname(dir);
|
|
23
|
+
if (parent === dir) break;
|
|
24
|
+
dir = parent;
|
|
25
|
+
}
|
|
26
|
+
return path.resolve(startDir || process.cwd());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveCompanionLoaderUrl(options = {}) {
|
|
30
|
+
if (options.src) {
|
|
31
|
+
return String(options.src);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const projectRoot = options.projectRoot || findProjectRoot(process.cwd());
|
|
35
|
+
const runtime = readRuntimeManifest(projectRoot);
|
|
36
|
+
if (runtime?.companion?.devLoader) {
|
|
37
|
+
return runtime.companion.devLoader;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const publicUrl = (
|
|
41
|
+
options.companionUrl ||
|
|
42
|
+
env('PUBLIC_URL', '') ||
|
|
43
|
+
''
|
|
44
|
+
).replace(/\/$/, '');
|
|
45
|
+
|
|
46
|
+
const port = String(options.port || env('PORT', '3344')).replace(/[^0-9]/g, '') || '3344';
|
|
47
|
+
const host = options.host || env('HOST', '127.0.0.1');
|
|
48
|
+
const base = publicUrl || `http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${port}`;
|
|
49
|
+
return `${base}/dev-loader.js`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
findProjectRoot,
|
|
54
|
+
resolveCompanionLoaderUrl
|
|
55
|
+
};
|
package/next.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional Next.js adapter — injects Crank via next/script (App Router or Pages).
|
|
5
|
+
*
|
|
6
|
+
* Prefer Vite plugin for Vite apps. Use this for Next.js when there is no vite.config.
|
|
7
|
+
*
|
|
8
|
+
* // app/layout.tsx (or a client layout section)
|
|
9
|
+
* import { CrankScript } from '@ahmednawaz/crank/next'
|
|
10
|
+
* <CrankScript />
|
|
11
|
+
*
|
|
12
|
+
* peerDependencies: react, next
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { resolveCompanionLoaderUrl } = require('./lib/loader-url');
|
|
16
|
+
const { CrankLoader } = require('./react');
|
|
17
|
+
|
|
18
|
+
function getReact() {
|
|
19
|
+
try {
|
|
20
|
+
return require('react');
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'@ahmednawaz/crank/next requires react. Prefer @ahmednawaz/crank/vite for Vite apps.'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getNextScript() {
|
|
29
|
+
try {
|
|
30
|
+
return require('next/script');
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {{ src?: string, companionUrl?: string, strategy?: string }} props
|
|
38
|
+
*/
|
|
39
|
+
function CrankScript(props) {
|
|
40
|
+
const React = getReact();
|
|
41
|
+
const NextScript = getNextScript();
|
|
42
|
+
const src = resolveCompanionLoaderUrl(props || {});
|
|
43
|
+
const strategy = (props && props.strategy) || 'afterInteractive';
|
|
44
|
+
|
|
45
|
+
if (NextScript) {
|
|
46
|
+
const Script = NextScript.default || NextScript;
|
|
47
|
+
return React.createElement(Script, {
|
|
48
|
+
src,
|
|
49
|
+
strategy,
|
|
50
|
+
'data-crank-loader': '1'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fallback when next/script is unavailable (tests / non-Next bundlers).
|
|
55
|
+
return React.createElement(CrankLoader, props || {});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
CrankScript,
|
|
60
|
+
CrankLoader,
|
|
61
|
+
Crank: CrankScript,
|
|
62
|
+
resolveCompanionLoaderUrl
|
|
63
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ahmednawaz/crank",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Internal Motive UX copy iteration: select UI text → Glean variants → MCP apply. Cursor, Replit, Claude.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,8 +14,21 @@
|
|
|
14
14
|
"exports": {
|
|
15
15
|
".": "./vite.js",
|
|
16
16
|
"./vite": "./vite.js",
|
|
17
|
+
"./react": "./react.js",
|
|
18
|
+
"./next": "./next.js",
|
|
19
|
+
"./webpack": "./webpack.js",
|
|
17
20
|
"./package.json": "./package.json"
|
|
18
21
|
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"html-webpack-plugin": ">=5",
|
|
24
|
+
"next": ">=13",
|
|
25
|
+
"react": ">=17"
|
|
26
|
+
},
|
|
27
|
+
"peerDependenciesMeta": {
|
|
28
|
+
"react": { "optional": true },
|
|
29
|
+
"next": { "optional": true },
|
|
30
|
+
"html-webpack-plugin": { "optional": true }
|
|
31
|
+
},
|
|
19
32
|
"workspaces": [
|
|
20
33
|
"companion",
|
|
21
34
|
"mcp-server"
|
|
@@ -57,6 +70,9 @@
|
|
|
57
70
|
"skill",
|
|
58
71
|
"panel",
|
|
59
72
|
"vite.js",
|
|
73
|
+
"react.js",
|
|
74
|
+
"next.js",
|
|
75
|
+
"webpack.js",
|
|
60
76
|
".env.example",
|
|
61
77
|
"AGENTS.md",
|
|
62
78
|
"REPLIT.md",
|
package/react.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional React adapter for non-Vite React apps (CRA, custom webpack, etc.).
|
|
5
|
+
*
|
|
6
|
+
* This is NOT a Retune-style UI component. It only injects the same /dev-loader.js
|
|
7
|
+
* script the Vite plugin uses. Prefer `@ahmednawaz/crank/vite` when the app uses Vite.
|
|
8
|
+
*
|
|
9
|
+
* import { CrankLoader } from '@ahmednawaz/crank/react'
|
|
10
|
+
* // in App / root layout (client-only):
|
|
11
|
+
* <CrankLoader />
|
|
12
|
+
*
|
|
13
|
+
* peerDependency: react >= 17
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { resolveCompanionLoaderUrl } = require('./lib/loader-url');
|
|
17
|
+
|
|
18
|
+
function getReact() {
|
|
19
|
+
try {
|
|
20
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
21
|
+
return require('react');
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error(
|
|
24
|
+
'@ahmednawaz/crank/react requires react as a peer dependency. Prefer the Vite plugin when possible: import { crank } from "@ahmednawaz/crank/vite".'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Injects Crank's companion loader once on the client. Renders null.
|
|
31
|
+
* @param {{ src?: string, companionUrl?: string, port?: string|number, host?: string }} props
|
|
32
|
+
*/
|
|
33
|
+
function CrankLoader(props) {
|
|
34
|
+
const React = getReact();
|
|
35
|
+
const src = resolveCompanionLoaderUrl(props || {});
|
|
36
|
+
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
if (typeof document === 'undefined') {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if (window.__CRANK_LOADER__) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
window.__CRANK_LOADER__ = true;
|
|
45
|
+
|
|
46
|
+
const existing = document.querySelector('script[data-crank-loader="1"]');
|
|
47
|
+
if (existing) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const script = document.createElement('script');
|
|
52
|
+
script.src = src;
|
|
53
|
+
script.defer = true;
|
|
54
|
+
script.dataset.crankLoader = '1';
|
|
55
|
+
document.documentElement.appendChild(script);
|
|
56
|
+
return undefined;
|
|
57
|
+
}, [src]);
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Alias — still only a script injector, not an in-tree panel UI. */
|
|
63
|
+
const Crank = CrankLoader;
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
CrankLoader,
|
|
67
|
+
Crank,
|
|
68
|
+
resolveCompanionLoaderUrl
|
|
69
|
+
};
|
package/skill/SKILL.md
CHANGED
|
@@ -33,6 +33,7 @@ When the user asks to start Crank:
|
|
|
33
33
|
```
|
|
34
34
|
`start` skips setup/init — companion on **3344** (+ HTTP MCP on **3345** on Replit). Add `CRANK_TEAM_TOKEN` in Secrets first.
|
|
35
35
|
4. Call **`crank_get_environment`** after the companion is up.
|
|
36
|
+
5. **Panel injection:** Vite apps use the Vite plugin (graceful `.crank/vite-plugin.cjs`) — not a React component. See **AGENTS.md** / `.crank/injection.md`. Keep companion running in a Workflow.
|
|
36
37
|
|
|
37
38
|
## When the user says "start Crank" or pastes `npx crank`
|
|
38
39
|
|
package/vite.js
CHANGED
|
@@ -1,53 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { env } = require('./lib/env');
|
|
6
|
-
const { readRuntimeManifest } = require('./lib/urls');
|
|
7
|
-
|
|
8
3
|
/**
|
|
9
4
|
* Vite / Rolldown-vite / Vitest-compatible plugin.
|
|
10
|
-
* Injects Crank into the app during `vite` / `npm run dev` — no
|
|
5
|
+
* Injects Crank into the app during `vite` / `npm run dev` — no React component needed.
|
|
11
6
|
*
|
|
12
7
|
* // vite.config.ts
|
|
13
|
-
* import { crank } from 'crank/vite'
|
|
8
|
+
* import { crank } from '@ahmednawaz/crank/vite'
|
|
9
|
+
* // or: import { crank } from 'crank/vite'
|
|
14
10
|
* export default { plugins: [crank()] }
|
|
11
|
+
*
|
|
12
|
+
* Prefer this for Vite apps (React, Vue, Svelte, vanilla). The panel is a script overlay,
|
|
13
|
+
* not a component in your React tree.
|
|
15
14
|
*/
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
let dir = path.resolve(startDir || process.cwd());
|
|
19
|
-
for (let i = 0; i < 8; i++) {
|
|
20
|
-
if (fs.existsSync(path.join(dir, '.crank', 'runtime.json'))) {
|
|
21
|
-
return dir;
|
|
22
|
-
}
|
|
23
|
-
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
24
|
-
return dir;
|
|
25
|
-
}
|
|
26
|
-
const parent = path.dirname(dir);
|
|
27
|
-
if (parent === dir) break;
|
|
28
|
-
dir = parent;
|
|
29
|
-
}
|
|
30
|
-
return path.resolve(startDir || process.cwd());
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function resolveCompanionLoaderUrl(options = {}) {
|
|
34
|
-
const projectRoot = options.projectRoot || findProjectRoot(process.cwd());
|
|
35
|
-
const runtime = readRuntimeManifest(projectRoot);
|
|
36
|
-
if (runtime?.companion?.devLoader) {
|
|
37
|
-
return runtime.companion.devLoader;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const publicUrl = (
|
|
41
|
-
options.companionUrl ||
|
|
42
|
-
env('PUBLIC_URL', '') ||
|
|
43
|
-
''
|
|
44
|
-
).replace(/\/$/, '');
|
|
45
|
-
|
|
46
|
-
const port = String(options.port || env('PORT', '3344')).replace(/[^0-9]/g, '') || '3344';
|
|
47
|
-
const host = options.host || env('HOST', '127.0.0.1');
|
|
48
|
-
const base = publicUrl || `http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${port}`;
|
|
49
|
-
return `${base}/dev-loader.js`;
|
|
50
|
-
}
|
|
16
|
+
const { resolveCompanionLoaderUrl } = require('./lib/loader-url');
|
|
51
17
|
|
|
52
18
|
function crank(options = {}) {
|
|
53
19
|
const src = resolveCompanionLoaderUrl(options);
|
package/webpack.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional webpack adapter — injects /dev-loader.js via html-webpack-plugin.
|
|
5
|
+
*
|
|
6
|
+
* // webpack.config.js
|
|
7
|
+
* const { crankWebpackPlugin } = require('@ahmednawaz/crank/webpack')
|
|
8
|
+
* plugins: [new HtmlWebpackPlugin(...), crankWebpackPlugin()]
|
|
9
|
+
*
|
|
10
|
+
* Prefer @ahmednawaz/crank/vite when the project uses Vite.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { resolveCompanionLoaderUrl } = require('./lib/loader-url');
|
|
14
|
+
|
|
15
|
+
function crankWebpackPlugin(options = {}) {
|
|
16
|
+
const src = resolveCompanionLoaderUrl(options);
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
apply(compiler) {
|
|
20
|
+
compiler.hooks.compilation.tap('CrankWebpackPlugin', (compilation) => {
|
|
21
|
+
let hooks;
|
|
22
|
+
try {
|
|
23
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
24
|
+
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
25
|
+
hooks = HtmlWebpackPlugin.getHooks(compilation);
|
|
26
|
+
} catch {
|
|
27
|
+
compilation.errors.push(
|
|
28
|
+
new Error(
|
|
29
|
+
'@ahmednawaz/crank/webpack requires html-webpack-plugin. Prefer the Vite plugin when possible.'
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
hooks.alterAssetTagGroups.tap('CrankWebpackPlugin', (data) => {
|
|
36
|
+
const already = (data.headTags || []).some(
|
|
37
|
+
(t) =>
|
|
38
|
+
t.tagName === 'script' &&
|
|
39
|
+
String(t.attributes?.src || '').includes('dev-loader.js')
|
|
40
|
+
);
|
|
41
|
+
if (!already) {
|
|
42
|
+
data.headTags.push({
|
|
43
|
+
tagName: 'script',
|
|
44
|
+
voidTag: false,
|
|
45
|
+
attributes: { src, defer: true, 'data-crank-loader': '1' }
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return data;
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
crankWebpackPlugin,
|
|
57
|
+
resolveCompanionLoaderUrl
|
|
58
|
+
};
|