@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 CHANGED
@@ -710,12 +710,9 @@ async function main() {
710
710
  });
711
711
  }
712
712
 
713
- // One-shot parity: setup/default always try inject on React apps (idempotent).
714
- if (
715
- (command === 'init' || command === 'run') &&
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
- exists(path.join(projectRoot, 'next.config.js')) ||
145
- exists(path.join(projectRoot, 'next.config.mjs')) ||
146
- exists(path.join(projectRoot, 'next.config.ts'));
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
- if (env.hasReact && options.react !== false) {
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) {
@@ -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.6",
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"