@ahmednawaz/crank 0.2.5 → 0.2.7
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/bin/crank.js +3 -6
- package/lib/detect.js +92 -8
- package/lib/init.js +148 -52
- package/lib/resolve-project.js +54 -28
- package/package.json +5 -3
package/bin/crank.js
CHANGED
|
@@ -710,12 +710,9 @@ async function main() {
|
|
|
710
710
|
});
|
|
711
711
|
}
|
|
712
712
|
|
|
713
|
-
// One-shot
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
detectEnv.hasReact &&
|
|
717
|
-
flags.mcp !== false
|
|
718
|
-
) {
|
|
713
|
+
// One-shot: always try <Crank /> inject (idempotent). Do not gate on hasReact —
|
|
714
|
+
// root package.json often omits react in monorepos / Replit layouts.
|
|
715
|
+
if ((command === 'init' || command === 'run') && flags.mcp !== false) {
|
|
719
716
|
const injectResult = injectCrank(projectRoot, { install: true });
|
|
720
717
|
initResult = initResult || { wrote: [], projectRoot };
|
|
721
718
|
initResult.inject = injectResult;
|
package/lib/detect.js
CHANGED
|
@@ -123,6 +123,91 @@ function packageHasDep(pkg, name) {
|
|
|
123
123
|
);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
const REACT_DEP_NAMES = [
|
|
127
|
+
'react',
|
|
128
|
+
'react-dom',
|
|
129
|
+
'@vitejs/plugin-react',
|
|
130
|
+
'@vitejs/plugin-react-swc',
|
|
131
|
+
'next'
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const REACT_ENTRY_HINTS = [
|
|
135
|
+
'src/main.tsx',
|
|
136
|
+
'src/main.jsx',
|
|
137
|
+
'src/index.tsx',
|
|
138
|
+
'src/index.jsx',
|
|
139
|
+
'client/src/main.tsx',
|
|
140
|
+
'app/main.tsx',
|
|
141
|
+
'main.tsx',
|
|
142
|
+
'main.jsx',
|
|
143
|
+
'index.tsx',
|
|
144
|
+
'index.jsx'
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
function packageLooksLikeReact(pkg) {
|
|
148
|
+
return REACT_DEP_NAMES.some((name) => packageHasDep(pkg, name));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function dirHasNextConfig(dir) {
|
|
152
|
+
return (
|
|
153
|
+
exists(path.join(dir, 'next.config.js')) ||
|
|
154
|
+
exists(path.join(dir, 'next.config.mjs')) ||
|
|
155
|
+
exists(path.join(dir, 'next.config.ts'))
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function dirHasReactEntryHint(dir) {
|
|
160
|
+
return REACT_ENTRY_HINTS.some((rel) => exists(path.join(dir, rel)));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* True when this directory (or a common app child) looks like a React app.
|
|
165
|
+
* Root package.json often omits react in monorepos / Replit multi-package layouts.
|
|
166
|
+
*/
|
|
167
|
+
function detectHasReact(projectRoot, packageJson, viteConfig) {
|
|
168
|
+
if (packageLooksLikeReact(packageJson) || dirHasNextConfig(projectRoot)) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (dirHasReactEntryHint(projectRoot)) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const dirs = new Set([projectRoot]);
|
|
176
|
+
for (const sub of APP_DIR_CANDIDATES) {
|
|
177
|
+
if (sub === '.') continue;
|
|
178
|
+
dirs.add(path.join(projectRoot, sub));
|
|
179
|
+
}
|
|
180
|
+
if (viteConfig) {
|
|
181
|
+
dirs.add(path.dirname(viteConfig));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const dir of dirs) {
|
|
185
|
+
if (!exists(dir) || dir === projectRoot) continue;
|
|
186
|
+
const pkg = readJson(path.join(dir, 'package.json'), null);
|
|
187
|
+
if (packageLooksLikeReact(pkg) || dirHasNextConfig(dir) || dirHasReactEntryHint(dir)) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Vite + JSX plugin import in config is a strong React signal
|
|
193
|
+
if (viteConfig && exists(viteConfig)) {
|
|
194
|
+
try {
|
|
195
|
+
const src = fs.readFileSync(viteConfig, 'utf8');
|
|
196
|
+
if (
|
|
197
|
+
/@vitejs\/plugin-react/.test(src) ||
|
|
198
|
+
/\bplugin-react\b/.test(src) ||
|
|
199
|
+
/from ['"]react['"]/.test(src)
|
|
200
|
+
) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
// ignore
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
126
211
|
function detectEnvironment(projectRoot) {
|
|
127
212
|
const env = process.env;
|
|
128
213
|
const isReplit = Boolean(
|
|
@@ -134,16 +219,13 @@ function detectEnvironment(projectRoot) {
|
|
|
134
219
|
: null;
|
|
135
220
|
const viteConfig = findViteConfig(projectRoot);
|
|
136
221
|
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'));
|
|
222
|
+
const hasReact = detectHasReact(projectRoot, packageJson, viteConfig);
|
|
142
223
|
const hasNext =
|
|
143
224
|
packageHasDep(packageJson, 'next') ||
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
225
|
+
dirHasNextConfig(projectRoot) ||
|
|
226
|
+
[...APP_DIR_CANDIDATES]
|
|
227
|
+
.filter((s) => s !== '.')
|
|
228
|
+
.some((sub) => dirHasNextConfig(path.join(projectRoot, sub)));
|
|
147
229
|
const hasWebpack =
|
|
148
230
|
packageHasDep(packageJson, 'webpack') ||
|
|
149
231
|
exists(path.join(projectRoot, 'webpack.config.js')) ||
|
|
@@ -182,6 +264,8 @@ module.exports = {
|
|
|
182
264
|
readJson,
|
|
183
265
|
writeJson,
|
|
184
266
|
detectEnvironment,
|
|
267
|
+
detectHasReact,
|
|
268
|
+
packageLooksLikeReact,
|
|
185
269
|
findViteConfig,
|
|
186
270
|
VITE_CONFIG_NAMES,
|
|
187
271
|
APP_DIR_CANDIDATES
|
package/lib/init.js
CHANGED
|
@@ -12,7 +12,7 @@ const {
|
|
|
12
12
|
|
|
13
13
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
14
14
|
const { resolveUrls } = require('./urls');
|
|
15
|
-
const { installCrankPackage } = require('./wire-react');
|
|
15
|
+
const { installCrankPackage, findEntry } = require('./wire-react');
|
|
16
16
|
|
|
17
17
|
function ensureGitignore(projectRoot) {
|
|
18
18
|
const file = path.join(projectRoot, '.gitignore');
|
|
@@ -187,6 +187,98 @@ module.exports = {
|
|
|
187
187
|
return stubPath;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
const NODE_IMPORT_SPECS = [
|
|
191
|
+
{ re: /import\s+\{\s*createRequire\s*\}\s+from\s+['"]node:module['"]\s*;?/, line: "import { createRequire } from 'node:module'" },
|
|
192
|
+
{ re: /import\s+\{\s*existsSync\s*\}\s+from\s+['"]node:fs['"]\s*;?/, line: "import { existsSync } from 'node:fs'" },
|
|
193
|
+
{ re: /import\s+\{\s*fileURLToPath\s*\}\s+from\s+['"]node:url['"]\s*;?/, line: "import { fileURLToPath } from 'node:url'" }
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
function hasCrankVitePatch(source) {
|
|
197
|
+
return (
|
|
198
|
+
/_crankPluginPath/.test(source) ||
|
|
199
|
+
/vite-plugin\.cjs/.test(source) ||
|
|
200
|
+
/Crank Vite plugin/.test(source) ||
|
|
201
|
+
(/\bcrank\s*\?\s*\[\s*crank\s*\(\s*\)\s*\]/.test(source) && /plugins/.test(source)) ||
|
|
202
|
+
(/(\bcrank)\s*\(/.test(source) && /plugins/.test(source) && /\.crank\//.test(source))
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Drop later duplicate ESM imports of the Node builtins Crank injects. */
|
|
207
|
+
function dedupeCrankNodeImports(source) {
|
|
208
|
+
let next = source;
|
|
209
|
+
let changed = false;
|
|
210
|
+
for (const { re } of NODE_IMPORT_SPECS) {
|
|
211
|
+
const globalRe = new RegExp(re.source, 'g');
|
|
212
|
+
const matches = next.match(globalRe);
|
|
213
|
+
if (!matches || matches.length < 2) continue;
|
|
214
|
+
let seen = 0;
|
|
215
|
+
next = next.replace(globalRe, (m) => {
|
|
216
|
+
seen += 1;
|
|
217
|
+
if (seen === 1) return m;
|
|
218
|
+
changed = true;
|
|
219
|
+
return '';
|
|
220
|
+
});
|
|
221
|
+
// Clean blank lines left by removals (keep file readable)
|
|
222
|
+
next = next.replace(/\n{3,}/g, '\n\n');
|
|
223
|
+
}
|
|
224
|
+
return { source: next, changed };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function missingNodeImportLines(source) {
|
|
228
|
+
return NODE_IMPORT_SPECS.filter(({ re }) => !re.test(source)).map(({ line }) => line);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function ensureCrankInPlugins(source) {
|
|
232
|
+
if (/\bcrank\s*\?\s*\[\s*crank\s*\(\s*\)\s*\]/.test(source) || /\[\s*\.\.\.\s*\(\s*crank\s*\?/.test(source)) {
|
|
233
|
+
return { source, changed: false, ok: true };
|
|
234
|
+
}
|
|
235
|
+
if (/plugins\s*:\s*\[\s*\]/.test(source)) {
|
|
236
|
+
return {
|
|
237
|
+
source: source.replace(/plugins\s*:\s*\[\s*\]/, 'plugins: [...(crank ? [crank()] : [])]'),
|
|
238
|
+
changed: true,
|
|
239
|
+
ok: true
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (/plugins\s*:\s*\[/.test(source)) {
|
|
243
|
+
return {
|
|
244
|
+
source: source.replace(/plugins\s*:\s*\[/, 'plugins: [...(crank ? [crank()] : []), '),
|
|
245
|
+
changed: true,
|
|
246
|
+
ok: true
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (/defineConfig\s*\(\s*\{/.test(source)) {
|
|
250
|
+
return {
|
|
251
|
+
source: source.replace(
|
|
252
|
+
/defineConfig\s*\(\s*\{/,
|
|
253
|
+
'defineConfig({\n plugins: [...(crank ? [crank()] : [])],'
|
|
254
|
+
),
|
|
255
|
+
changed: true,
|
|
256
|
+
ok: true
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (/export\s+default\s+\{/.test(source)) {
|
|
260
|
+
return {
|
|
261
|
+
source: source.replace(
|
|
262
|
+
/export\s+default\s+\{/,
|
|
263
|
+
'export default {\n plugins: [...(crank ? [crank()] : [])],'
|
|
264
|
+
),
|
|
265
|
+
changed: true,
|
|
266
|
+
ok: true
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (/module\.exports\s*=\s*\{/.test(source)) {
|
|
270
|
+
return {
|
|
271
|
+
source: source.replace(
|
|
272
|
+
/module\.exports\s*=\s*\{/,
|
|
273
|
+
'module.exports = {\n plugins: [...(crank ? [crank()] : [])],'
|
|
274
|
+
),
|
|
275
|
+
changed: true,
|
|
276
|
+
ok: true
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
return { source, changed: false, ok: false };
|
|
280
|
+
}
|
|
281
|
+
|
|
190
282
|
function patchViteConfig(projectRoot) {
|
|
191
283
|
const configPath = findViteConfig(projectRoot);
|
|
192
284
|
const stubPath = writeVitePluginStub(projectRoot);
|
|
@@ -196,11 +288,21 @@ function patchViteConfig(projectRoot) {
|
|
|
196
288
|
}
|
|
197
289
|
|
|
198
290
|
let source = fs.readFileSync(configPath, 'utf8');
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
) {
|
|
203
|
-
|
|
291
|
+
|
|
292
|
+
// Heal configs broken by older Crank versions (duplicate node: imports).
|
|
293
|
+
const repaired = dedupeCrankNodeImports(source);
|
|
294
|
+
if (repaired.changed) {
|
|
295
|
+
source = repaired.source;
|
|
296
|
+
fs.writeFileSync(configPath, source);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (hasCrankVitePatch(source)) {
|
|
300
|
+
return {
|
|
301
|
+
patched: repaired.changed,
|
|
302
|
+
reason: repaired.changed ? 'repaired-duplicate-imports' : 'already-present',
|
|
303
|
+
file: configPath,
|
|
304
|
+
plugin: stubPath
|
|
305
|
+
};
|
|
204
306
|
}
|
|
205
307
|
|
|
206
308
|
let relStub = path.relative(path.dirname(configPath), stubPath).replace(/\\/g, '/');
|
|
@@ -218,11 +320,8 @@ function patchViteConfig(projectRoot) {
|
|
|
218
320
|
const crankDecl = isTs
|
|
219
321
|
? 'let crank: null | (() => unknown) = null'
|
|
220
322
|
: 'let crank = null';
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
import { fileURLToPath } from 'node:url'
|
|
224
|
-
|
|
225
|
-
// Crank Vite plugin — graceful load (same pattern as Vizpatch); skip if unavailable
|
|
323
|
+
const importLines = missingNodeImportLines(source);
|
|
324
|
+
const gracefulBlock = `${importLines.length ? `${importLines.join('\n')}\n\n` : ''}// Crank Vite plugin — graceful load; skip if unavailable
|
|
226
325
|
${crankDecl}
|
|
227
326
|
try {
|
|
228
327
|
const _crankPluginPath = fileURLToPath(new URL('${relStub}', import.meta.url))
|
|
@@ -233,31 +332,14 @@ try {
|
|
|
233
332
|
} catch {}
|
|
234
333
|
|
|
235
334
|
`;
|
|
335
|
+
// If the file already uses createRequire/existsSync/fileURLToPath without
|
|
336
|
+
// importing them, missingNodeImportLines still adds the needed imports once.
|
|
236
337
|
if (!/_crankPluginPath/.test(source)) {
|
|
237
338
|
source = gracefulBlock + source;
|
|
238
339
|
}
|
|
239
340
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
/plugins\s*:\s*\[\s*\]/,
|
|
243
|
-
'plugins: [...(crank ? [crank()] : [])]'
|
|
244
|
-
);
|
|
245
|
-
} else if (/plugins\s*:\s*\[/.test(source)) {
|
|
246
|
-
source = source.replace(
|
|
247
|
-
/plugins\s*:\s*\[/,
|
|
248
|
-
'plugins: [...(crank ? [crank()] : []), '
|
|
249
|
-
);
|
|
250
|
-
} else if (/defineConfig\s*\(\s*\{/.test(source)) {
|
|
251
|
-
source = source.replace(
|
|
252
|
-
/defineConfig\s*\(\s*\{/,
|
|
253
|
-
'defineConfig({\n plugins: [...(crank ? [crank()] : [])],'
|
|
254
|
-
);
|
|
255
|
-
} else if (/export\s+default\s+\{/.test(source)) {
|
|
256
|
-
source = source.replace(
|
|
257
|
-
/export\s+default\s+\{/,
|
|
258
|
-
'export default {\n plugins: [...(crank ? [crank()] : [])],'
|
|
259
|
-
);
|
|
260
|
-
} else {
|
|
341
|
+
const plugged = ensureCrankInPlugins(source);
|
|
342
|
+
if (!plugged.ok) {
|
|
261
343
|
return {
|
|
262
344
|
patched: false,
|
|
263
345
|
reason: 'unrecognized-vite-shape',
|
|
@@ -265,33 +347,20 @@ try {
|
|
|
265
347
|
plugin: stubPath
|
|
266
348
|
};
|
|
267
349
|
}
|
|
350
|
+
source = plugged.source;
|
|
268
351
|
} else {
|
|
269
|
-
const gracefulBlock = `// Crank Vite plugin — graceful load
|
|
352
|
+
const gracefulBlock = `// Crank Vite plugin — graceful load; skip if unavailable
|
|
270
353
|
let crank = null;
|
|
271
354
|
try {
|
|
272
355
|
crank = require('${relStub}').crank;
|
|
273
356
|
} catch {}
|
|
274
357
|
|
|
275
358
|
`;
|
|
276
|
-
if (!/vite-plugin\.cjs/.test(source)) {
|
|
359
|
+
if (!/vite-plugin\.cjs/.test(source) && !/_crankPluginPath/.test(source)) {
|
|
277
360
|
source = gracefulBlock + source;
|
|
278
361
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
/plugins\s*:\s*\[\s*\]/,
|
|
282
|
-
'plugins: [...(crank ? [crank()] : [])]'
|
|
283
|
-
);
|
|
284
|
-
} else if (/plugins\s*:\s*\[/.test(source)) {
|
|
285
|
-
source = source.replace(
|
|
286
|
-
/plugins\s*:\s*\[/,
|
|
287
|
-
'plugins: [...(crank ? [crank()] : []), '
|
|
288
|
-
);
|
|
289
|
-
} else if (/module\.exports\s*=\s*\{/.test(source)) {
|
|
290
|
-
source = source.replace(
|
|
291
|
-
/module\.exports\s*=\s*\{/,
|
|
292
|
-
'module.exports = {\n plugins: [...(crank ? [crank()] : [])],'
|
|
293
|
-
);
|
|
294
|
-
} else {
|
|
362
|
+
const plugged = ensureCrankInPlugins(source);
|
|
363
|
+
if (!plugged.ok) {
|
|
295
364
|
return {
|
|
296
365
|
patched: false,
|
|
297
366
|
reason: 'unrecognized-vite-shape',
|
|
@@ -299,12 +368,27 @@ try {
|
|
|
299
368
|
plugin: stubPath
|
|
300
369
|
};
|
|
301
370
|
}
|
|
371
|
+
source = plugged.source;
|
|
302
372
|
}
|
|
303
373
|
|
|
374
|
+
// Final safety: never leave duplicate node imports.
|
|
375
|
+
source = dedupeCrankNodeImports(source).source;
|
|
376
|
+
|
|
304
377
|
fs.writeFileSync(configPath, source);
|
|
305
378
|
return { patched: true, file: configPath, plugin: stubPath };
|
|
306
379
|
}
|
|
307
380
|
|
|
381
|
+
/** Repair-only: dedupe node: imports left by older Crank Vite patches (React path). */
|
|
382
|
+
function repairViteConfigDuplicates(projectRoot) {
|
|
383
|
+
const configPath = findViteConfig(projectRoot);
|
|
384
|
+
if (!configPath) return { repaired: false, reason: 'no-vite-config' };
|
|
385
|
+
const source = fs.readFileSync(configPath, 'utf8');
|
|
386
|
+
const repaired = dedupeCrankNodeImports(source);
|
|
387
|
+
if (!repaired.changed) return { repaired: false, reason: 'clean', file: configPath };
|
|
388
|
+
fs.writeFileSync(configPath, repaired.source);
|
|
389
|
+
return { repaired: true, reason: 'repaired-duplicate-imports', file: configPath };
|
|
390
|
+
}
|
|
391
|
+
|
|
308
392
|
function writeProjectEnvExample(projectRoot) {
|
|
309
393
|
const dest = path.join(projectRoot, '.env.crank.example');
|
|
310
394
|
const src = path.join(PACKAGE_ROOT, '.env.example');
|
|
@@ -536,10 +620,21 @@ function initProject(projectRoot, options = {}) {
|
|
|
536
620
|
installCrankSkill(projectRoot, results);
|
|
537
621
|
}
|
|
538
622
|
|
|
539
|
-
// React apps:
|
|
540
|
-
|
|
623
|
+
// React apps: <Crank /> only — do not inject the Vite plugin (avoids duplicating
|
|
624
|
+
// node: imports when vite.config already has Vizpatch-style loaders).
|
|
625
|
+
// Still repair duplicate imports left by older Crank versions.
|
|
626
|
+
// Treat findEntry hits as React even when root package.json omits the dep.
|
|
627
|
+
const looksLikeReact = env.hasReact || Boolean(findEntry(projectRoot));
|
|
628
|
+
if (looksLikeReact && options.react !== false) {
|
|
541
629
|
writeReactMountHint(projectRoot, env, results);
|
|
542
630
|
results.reactMount = true;
|
|
631
|
+
if (env.hasVite) {
|
|
632
|
+
const repaired = repairViteConfigDuplicates(projectRoot);
|
|
633
|
+
if (repaired.repaired) {
|
|
634
|
+
results.vite = repaired;
|
|
635
|
+
results.wrote.push(path.relative(projectRoot, repaired.file));
|
|
636
|
+
}
|
|
637
|
+
}
|
|
543
638
|
} else if (env.hasVite && options.vite !== false) {
|
|
544
639
|
const viteResult = patchViteConfig(projectRoot);
|
|
545
640
|
results.vite = viteResult;
|
|
@@ -901,6 +996,7 @@ module.exports = {
|
|
|
901
996
|
writeReplitPlatformGuide,
|
|
902
997
|
writeVitePluginStub,
|
|
903
998
|
patchViteConfig,
|
|
999
|
+
repairViteConfigDuplicates,
|
|
904
1000
|
writeInjectionGuide,
|
|
905
1001
|
writeReactMountHint,
|
|
906
1002
|
patchReplitConfig,
|
package/lib/resolve-project.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { execSync } = require('child_process');
|
|
6
|
-
const { exists, readJson } = require('./detect');
|
|
6
|
+
const { exists, readJson, findViteConfig, packageLooksLikeReact } = require('./detect');
|
|
7
7
|
|
|
8
8
|
const APP_DIR_CANDIDATES = [
|
|
9
9
|
'.',
|
|
@@ -37,6 +37,59 @@ function hasFrameworkMarker(dir) {
|
|
|
37
37
|
return FRAMEWORK_FILES.some((name) => exists(path.join(dir, name)));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function dirPackageLooksLikeReact(dir) {
|
|
41
|
+
return packageLooksLikeReact(readJson(path.join(dir, 'package.json'), null));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Prefer the real app package (Vite/React) over a bare repo-root package.json.
|
|
46
|
+
*/
|
|
47
|
+
function findAppDirectory(repoRoot) {
|
|
48
|
+
const root = path.resolve(repoRoot);
|
|
49
|
+
|
|
50
|
+
// 1. Known app subdirs with a framework config (before root — monorepos)
|
|
51
|
+
for (const rel of APP_DIR_CANDIDATES) {
|
|
52
|
+
if (rel === '.') continue;
|
|
53
|
+
const candidate = path.join(root, rel);
|
|
54
|
+
if (exists(candidate) && hasFrameworkMarker(candidate)) {
|
|
55
|
+
return { appRoot: candidate, reason: `framework in ${rel}/` };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Vite config found via shallow search (e.g. artifacts/<app>/ on Replit)
|
|
60
|
+
const viteConfig = findViteConfig(root);
|
|
61
|
+
if (viteConfig) {
|
|
62
|
+
const viteDir = path.dirname(viteConfig);
|
|
63
|
+
if (viteDir !== root) {
|
|
64
|
+
// Prefer the vite package dir when it looks like React, or root does not.
|
|
65
|
+
if (dirPackageLooksLikeReact(viteDir) || !dirPackageLooksLikeReact(root)) {
|
|
66
|
+
return {
|
|
67
|
+
appRoot: viteDir,
|
|
68
|
+
reason: `vite.config in ${path.relative(root, viteDir) || '.'}`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 3. Root has framework
|
|
75
|
+
if (hasFrameworkMarker(root)) {
|
|
76
|
+
return { appRoot: root, reason: 'framework at repo root' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 4. Subdir / root package.json
|
|
80
|
+
for (const rel of APP_DIR_CANDIDATES) {
|
|
81
|
+
const candidate = path.join(root, rel === '.' ? '' : rel);
|
|
82
|
+
if (exists(candidate) && exists(path.join(candidate, 'package.json'))) {
|
|
83
|
+
return {
|
|
84
|
+
appRoot: candidate,
|
|
85
|
+
reason: `package.json in ${rel === '.' ? 'root' : rel + '/'}`
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { appRoot: root, reason: 'repo root (fallback)' };
|
|
91
|
+
}
|
|
92
|
+
|
|
40
93
|
function readCrankConfig(startDir) {
|
|
41
94
|
let dir = path.resolve(startDir);
|
|
42
95
|
for (let i = 0; i < 12; i++) {
|
|
@@ -95,33 +148,6 @@ function findPackageRoot(startDir) {
|
|
|
95
148
|
return path.resolve(startDir);
|
|
96
149
|
}
|
|
97
150
|
|
|
98
|
-
/**
|
|
99
|
-
* Retune-style: find the app directory inside a repo (app/, client/, web/, …).
|
|
100
|
-
*/
|
|
101
|
-
function findAppDirectory(repoRoot) {
|
|
102
|
-
const root = path.resolve(repoRoot);
|
|
103
|
-
if (hasFrameworkMarker(root)) {
|
|
104
|
-
return { appRoot: root, reason: 'framework at repo root' };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
for (const rel of APP_DIR_CANDIDATES) {
|
|
108
|
-
if (rel === '.') continue;
|
|
109
|
-
const candidate = path.join(root, rel);
|
|
110
|
-
if (exists(candidate) && hasFrameworkMarker(candidate)) {
|
|
111
|
-
return { appRoot: candidate, reason: `framework in ${rel}/` };
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
for (const rel of APP_DIR_CANDIDATES) {
|
|
116
|
-
const candidate = path.join(root, rel === '.' ? '' : rel);
|
|
117
|
-
if (exists(candidate) && exists(path.join(candidate, 'package.json'))) {
|
|
118
|
-
return { appRoot: candidate, reason: `package.json in ${rel === '.' ? 'root' : rel + '/'}` };
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return { appRoot: root, reason: 'repo root (fallback)' };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
151
|
/**
|
|
126
152
|
* Resolve where the user's app code lives — no manual path required.
|
|
127
153
|
* Priority: CRANK_PROJECT_ROOT → .crank/config → git root + app dir → package.json walk → cwd.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ahmednawaz/crank",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Internal Motive UX copy iteration: select UI text
|
|
3
|
+
"version": "0.2.7",
|
|
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",
|
|
7
7
|
"url": "https://github.com/ahmednawaz619/crank.git"
|
|
@@ -66,7 +66,9 @@
|
|
|
66
66
|
"test:resolve": "node scripts/test-resolve-project.js",
|
|
67
67
|
"prepare:publish": "node scripts/prepare-publish.js",
|
|
68
68
|
"verify:deps": "node scripts/verify-deps.js",
|
|
69
|
-
"publish:team": "node scripts/prepare-publish.js && node scripts/verify-deps.js && npm publish --access public"
|
|
69
|
+
"publish:team": "node scripts/prepare-publish.js && node scripts/verify-deps.js && npm publish --access public",
|
|
70
|
+
"test:patch-vite": "node scripts/test-patch-vite.js",
|
|
71
|
+
"test:react-detect": "node scripts/test-react-detect.js"
|
|
70
72
|
},
|
|
71
73
|
"engines": {
|
|
72
74
|
"node": ">=18"
|