@ahmednawaz/crank 0.2.6 → 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 +4 -2
- package/lib/resolve-project.js +54 -28
- package/package.json +3 -2
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');
|
|
@@ -623,7 +623,9 @@ function initProject(projectRoot, options = {}) {
|
|
|
623
623
|
// React apps: <Crank /> only — do not inject the Vite plugin (avoids duplicating
|
|
624
624
|
// node: imports when vite.config already has Vizpatch-style loaders).
|
|
625
625
|
// Still repair duplicate imports left by older Crank versions.
|
|
626
|
-
|
|
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) {
|
|
627
629
|
writeReactMountHint(projectRoot, env, results);
|
|
628
630
|
results.reactMount = true;
|
|
629
631
|
if (env.hasVite) {
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ahmednawaz/crank",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
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",
|
|
@@ -67,7 +67,8 @@
|
|
|
67
67
|
"prepare:publish": "node scripts/prepare-publish.js",
|
|
68
68
|
"verify:deps": "node scripts/verify-deps.js",
|
|
69
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"
|
|
70
|
+
"test:patch-vite": "node scripts/test-patch-vite.js",
|
|
71
|
+
"test:react-detect": "node scripts/test-react-detect.js"
|
|
71
72
|
},
|
|
72
73
|
"engines": {
|
|
73
74
|
"node": ">=18"
|