@ahmednawaz/crank 0.2.5 → 0.2.6

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.
Files changed (2) hide show
  1. package/lib/init.js +144 -50
  2. package/package.json +4 -3
package/lib/init.js CHANGED
@@ -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
- if (
200
- /vite-plugin\.cjs/.test(source) ||
201
- (/(\bcrank)\s*\(/.test(source) && /plugins/.test(source))
202
- ) {
203
- return { patched: false, reason: 'already-present', file: configPath, plugin: stubPath };
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 gracefulBlock = `import { createRequire } from 'node:module'
222
- import { existsSync } from 'node:fs'
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
- if (/plugins\s*:\s*\[\s*\]/.test(source)) {
241
- source = source.replace(
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 (same pattern as Vizpatch)
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
- if (/plugins\s*:\s*\[\s*\]/.test(source)) {
280
- source = source.replace(
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,19 @@ function initProject(projectRoot, options = {}) {
536
620
  installCrankSkill(projectRoot, results);
537
621
  }
538
622
 
539
- // React apps: install dep + write out inject (Sentry/Clerk-style). Do not patch source here.
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.
540
626
  if (env.hasReact && options.react !== false) {
541
627
  writeReactMountHint(projectRoot, env, results);
542
628
  results.reactMount = true;
629
+ if (env.hasVite) {
630
+ const repaired = repairViteConfigDuplicates(projectRoot);
631
+ if (repaired.repaired) {
632
+ results.vite = repaired;
633
+ results.wrote.push(path.relative(projectRoot, repaired.file));
634
+ }
635
+ }
543
636
  } else if (env.hasVite && options.vite !== false) {
544
637
  const viteResult = patchViteConfig(projectRoot);
545
638
  results.vite = viteResult;
@@ -901,6 +994,7 @@ module.exports = {
901
994
  writeReplitPlatformGuide,
902
995
  writeVitePluginStub,
903
996
  patchViteConfig,
997
+ repairViteConfigDuplicates,
904
998
  writeInjectionGuide,
905
999
  writeReactMountHint,
906
1000
  patchReplitConfig,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ahmednawaz/crank",
3
- "version": "0.2.5",
4
- "description": "Internal Motive UX copy iteration: select UI text Glean variants MCP apply. Cursor, Replit, Claude.",
3
+ "version": "0.2.6",
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,8 @@
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"
70
71
  },
71
72
  "engines": {
72
73
  "node": ">=18"