@rocket/js 0.1.0 → 0.1.2

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 (43) hide show
  1. package/README.md +20 -26
  2. package/dist-types/exports/components.d.ts +1 -1
  3. package/dist-types/exports/icons.d.ts +1 -1
  4. package/dist-types/exports/layout.d.ts +1 -1
  5. package/dist-types/exports/types/rocket.d.ts +8 -0
  6. package/dist-types/exports/types/rocket.d.ts.map +1 -1
  7. package/dist-types/src/cli/RocketBuild.d.ts.map +1 -1
  8. package/dist-types/src/cli/RocketInit.d.ts +5 -1
  9. package/dist-types/src/cli/RocketInit.d.ts.map +1 -1
  10. package/dist-types/src/cli/RocketStart.d.ts +34 -2
  11. package/dist-types/src/cli/RocketStart.d.ts.map +1 -1
  12. package/dist-types/src/components.d.ts +2 -0
  13. package/dist-types/src/components.d.ts.map +1 -1
  14. package/dist-types/src/icons.d.ts +12 -0
  15. package/dist-types/src/icons.d.ts.map +1 -1
  16. package/dist-types/src/layouts/atlas/atlasDocLayout.d.ts +4 -0
  17. package/dist-types/src/layouts/atlas/atlasDocLayout.d.ts.map +1 -1
  18. package/dist-types/src/layouts/atlas/atlasHeroLayout.d.ts.map +1 -1
  19. package/dist-types/src/layouts/atlas/atlasNotFoundLayout.d.ts.map +1 -1
  20. package/dist-types/src/layouts/layout.d.ts +2 -4
  21. package/dist-types/src/layouts/layout.d.ts.map +1 -1
  22. package/dist-types/src/standalone-demo-url.d.ts.map +1 -1
  23. package/dist-types/src/transform.d.ts.map +1 -1
  24. package/dist-types/src/wds-plugin.d.ts +1 -0
  25. package/dist-types/src/wds-plugin.d.ts.map +1 -1
  26. package/exports/components.js +1 -1
  27. package/exports/icons.js +3 -0
  28. package/exports/layout.js +1 -1
  29. package/exports/types/rocket.ts +8 -0
  30. package/package.json +2 -2
  31. package/src/cli/RocketBuild.js +38 -2
  32. package/src/cli/RocketInit.js +401 -36
  33. package/src/cli/RocketStart.js +96 -30
  34. package/src/components.js +19 -0
  35. package/src/icons.js +15 -0
  36. package/src/layouts/atlas/atlasDocLayout.js +10 -15
  37. package/src/layouts/atlas/atlasHeroLayout.js +10 -13
  38. package/src/layouts/atlas/atlasNotFoundLayout.js +5 -3
  39. package/src/layouts/layout.js +2 -12
  40. package/src/main.js +21 -4
  41. package/src/standalone-demo-url.js +21 -8
  42. package/src/transform.js +89 -2
  43. package/src/wds-plugin.js +14 -9
@@ -1,10 +1,26 @@
1
1
  /* eslint-disable no-console */
2
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
 
5
5
  const ROCKET_CONFIG_PATH = 'rocket-config.js';
6
6
  const INDEX_PAGE_PATH = 'docs/pages/index.rocket.md';
7
+ const SHARED_DATA_PATH = 'docs/pages/sharedData.js';
8
+ const THEME_CSS_PATH = 'public/rocket-theme.css';
9
+ const DOCS_PAGE_PATH = 'docs/pages/docs.rocket.md';
10
+ const JAVASCRIPT_DEMO_PAGE_PATH = 'docs/pages/javascript-demo.rocket.md';
11
+ const REQUEST_DEMO_PAGE_PATH = 'docs/pages/request-demo.rocket.md';
12
+ const SITE_STATUS_PAGE_PATH = 'docs/pages/site-status.rocket.js';
7
13
  const ROCKET_AGENT_SKILL_PATH = '.agents/skills/rocket/SKILL.md';
14
+ const EXISTING_ROCKET_PAGE_NOISE_THRESHOLD = 3;
15
+ const STARTER_PAGE_FILES = [
16
+ SHARED_DATA_PATH,
17
+ THEME_CSS_PATH,
18
+ INDEX_PAGE_PATH,
19
+ DOCS_PAGE_PATH,
20
+ JAVASCRIPT_DEMO_PAGE_PATH,
21
+ REQUEST_DEMO_PAGE_PATH,
22
+ SITE_STATUS_PAGE_PATH,
23
+ ];
8
24
 
9
25
  const rocketConfigSource = `/** @type {import('@rocket/js/types.js').RocketConfig} */
10
26
  export default {
@@ -12,6 +28,108 @@ export default {
12
28
  };
13
29
  `;
14
30
 
31
+ const sharedDataSource = `import { resolve } from '@rocket/js/resolve.js';
32
+
33
+ const themeStylesheets = ['/rocket-theme.css'];
34
+
35
+ export const headerData = {
36
+ logo: [
37
+ resolve('@rocket/js/docs/assets/rocket-logo-light.svg', import.meta),
38
+ resolve('@rocket/js/docs/assets/rocket-text-no-logo.svg', import.meta),
39
+ ],
40
+ homeLink: '/',
41
+ navLinks: [
42
+ { text: 'Docs', href: '/docs' },
43
+ { text: 'Demos', href: '/javascript-demo' },
44
+ ],
45
+ socials: [],
46
+ };
47
+
48
+ /** @type {import('@rocket/js/types.js').FooterSection[]} */
49
+ export const footerData = [];
50
+
51
+ export const heroData = {
52
+ headerData,
53
+ footerData,
54
+ stylesheets: themeStylesheets,
55
+ heroMainData: {
56
+ logoNoText: resolve('@rocket/js/docs/assets/rocket-logo-light.svg', import.meta),
57
+ eyebrow: 'ROCKET STARTER',
58
+ title: 'Rocket Site',
59
+ body: 'A static documentation site built with Rocket Pages and Atlas layouts.',
60
+ documentationLink: '/docs',
61
+ documentationText: 'Read the docs',
62
+ setupLink: '/javascript-demo',
63
+ setupText: 'View demos',
64
+ installLabel: 'Build',
65
+ installCommand: 'npm run build',
66
+ },
67
+ whyRocketData: [
68
+ {
69
+ icon: 'file-earmark-text',
70
+ title: 'Plain Pages',
71
+ description: 'Author durable content in Markdown and keep routes explicit.',
72
+ },
73
+ {
74
+ icon: 'play-btn',
75
+ title: 'Live Demos',
76
+ description: 'Use JavaScript Demos for browser examples with standalone URLs.',
77
+ },
78
+ {
79
+ icon: 'arrow-left-right',
80
+ title: 'Request Demos',
81
+ description: 'Show concrete same-site responses without leaving the docs.',
82
+ },
83
+ ],
84
+ quickStartData: {
85
+ title: 'Quick start',
86
+ subtitle: 'From this project:',
87
+ command: ['npm start', 'npm run build'],
88
+ description: 'Edit docs/pages, then run the build before publishing.',
89
+ },
90
+ workflowData: {
91
+ title: 'Starter workflow',
92
+ steps: [
93
+ {
94
+ icon: 'pencil-square',
95
+ title: 'Edit Pages',
96
+ description: 'Update the Markdown and JavaScript Pages under docs/pages.',
97
+ },
98
+ {
99
+ icon: 'terminal',
100
+ title: 'Build',
101
+ description: 'Run npm run build and fix any errors before publishing.',
102
+ },
103
+ ],
104
+ },
105
+ };
106
+
107
+ export const docData = {
108
+ headerData,
109
+ footerData,
110
+ stylesheets: themeStylesheets,
111
+ navigationIconServerBudget: 35,
112
+ };
113
+ `;
114
+
115
+ const themeCssSource = `:root {
116
+ --rocket-theme-primary: #e10d14;
117
+ --rocket-theme-primary-dark: #b90f12;
118
+ --rocket-theme-link: #0366d6;
119
+ }
120
+
121
+ .atlas-page,
122
+ .atlas-header,
123
+ .atlas-navigation,
124
+ .atlas-toc,
125
+ .home-main,
126
+ rocket-header {
127
+ --primary-color: var(--rocket-theme-primary);
128
+ --primary-color-dark: var(--rocket-theme-primary-dark);
129
+ --link-color: var(--rocket-theme-link);
130
+ }
131
+ `;
132
+
15
133
  const indexPageSource = `\`\`\`js server
16
134
  export const config = {
17
135
  path: '/',
@@ -19,23 +137,166 @@ export const config = {
19
137
  title: 'Rocket Site',
20
138
  description: 'Documentation built with Rocket.',
21
139
  },
140
+ menu: {
141
+ iconName: 'house',
142
+ order: 0,
143
+ },
22
144
  };
23
145
 
24
- export { layout } from '@rocket/js/layout.js';
146
+ import { atlasHeroLayout, atlasHeroComponents } from '@rocket/js/layouts/atlasHero.js';
147
+ import { heroData } from './sharedData.js';
148
+
149
+ export const components = atlasHeroComponents;
150
+ export const layout = pageData => atlasHeroLayout(pageData, heroData);
25
151
  \`\`\`
26
152
 
27
153
  # Rocket Site
28
154
 
29
- This Page is rendered by Rocket.
155
+ This starter is rendered with Rocket's Atlas hero layout.
30
156
 
31
157
  ## Next steps
32
158
 
33
159
  - Edit this Page in \`docs/pages/index.rocket.md\`.
34
- - Add general documentation Pages under \`docs/pages\`.
35
- - Add component reference Pages next to the components they document.
160
+ - Edit the shared Atlas data in \`docs/pages/sharedData.js\`.
161
+ - Add general documentation Pages under \`docs/pages\` with the Atlas docs layout.
162
+ - Add component reference Pages next to the components they document under \`src\`.
36
163
  - Run \`npm run build\` to verify the site.
37
164
  `;
38
165
 
166
+ const docsPageSource = `\`\`\`js server
167
+ export const config = {
168
+ path: '/docs',
169
+ metadata: {
170
+ title: 'Docs',
171
+ description: 'A first Atlas docs Page generated by rocket init.',
172
+ },
173
+ menu: {
174
+ iconName: 'book',
175
+ order: 10,
176
+ },
177
+ };
178
+
179
+ import { atlasDocLayout, atlasDocComponents } from '@rocket/js/layouts/atlasDoc.js';
180
+ import { docData } from './sharedData.js';
181
+
182
+ export const components = atlasDocComponents;
183
+ export const layout = pageData => atlasDocLayout(pageData, docData);
184
+ \`\`\`
185
+
186
+ # Docs
187
+
188
+ This Page uses Rocket's package-provided Atlas docs layout.
189
+
190
+ ## Authoring Pages
191
+
192
+ Every Page owns its URL with \`config.path\`. General documentation Pages belong under
193
+ \`docs/pages\`, and component reference Pages can live next to the source files they document.
194
+
195
+ ## Navigation Icons
196
+
197
+ Atlas docs navigation reads \`menu.iconName\` from Page config. Use Bootstrap Icon names for Pages
198
+ that appear in the left navigation.
199
+ `;
200
+
201
+ const javascriptDemoPageSource = `\`\`\`js server
202
+ export const config = {
203
+ path: '/javascript-demo',
204
+ metadata: {
205
+ title: 'JavaScript Demo',
206
+ description: 'A starter JavaScript Demo with a generated Standalone Demo URL.',
207
+ },
208
+ menu: {
209
+ iconName: 'window',
210
+ order: 20,
211
+ },
212
+ };
213
+
214
+ import { atlasDocLayout, atlasDocComponents } from '@rocket/js/layouts/atlasDoc.js';
215
+ import { docData } from './sharedData.js';
216
+
217
+ export const components = atlasDocComponents;
218
+ export const layout = pageData => atlasDocLayout(pageData, docData);
219
+ \`\`\`
220
+
221
+ # JavaScript Demo
222
+
223
+ Use a \`js demo\` block for browser-rendered examples. Rocket also generates a Standalone Demo URL
224
+ at \`/javascript-demo/_demo/starterButton/\`.
225
+
226
+ \`\`\`js demo label="docs/pages/javascript-demo.rocket.md"
227
+ import { html } from 'lit';
228
+
229
+ export const starterButton = () => html\`<button type="button">Rocket demo</button>\`;
230
+ \`\`\`
231
+ `;
232
+
233
+ const requestDemoPageSource = `\`\`\`js server
234
+ export const config = {
235
+ path: '/request-demo',
236
+ metadata: {
237
+ title: 'Request Demo',
238
+ description: 'A starter Request Demo pointed at a concrete static JSON Page.',
239
+ },
240
+ menu: {
241
+ iconName: 'arrow-left-right',
242
+ order: 30,
243
+ },
244
+ };
245
+
246
+ import { atlasDocLayout, atlasDocComponents } from '@rocket/js/layouts/atlasDoc.js';
247
+ import { docData } from './sharedData.js';
248
+
249
+ export const components = atlasDocComponents;
250
+ export const layout = pageData => atlasDocLayout(pageData, docData);
251
+ \`\`\`
252
+
253
+ # Request Demo
254
+
255
+ Use a Request Demo when readers need to inspect a same-site \`GET\` response. Static Request Demo
256
+ examples should point at concrete paths and avoid query-dependent output.
257
+
258
+ \`\`\`js request-demo url="/api/site-status.json" label="docs/pages/site-status.rocket.js" height=220
259
+ export const config = {
260
+ path: '/api/site-status.json',
261
+ metadata: { title: 'Site Status' },
262
+ menu: false,
263
+ };
264
+
265
+ export default function siteStatusPage() {
266
+ return {
267
+ status: 'ready',
268
+ generatedBy: 'Rocket',
269
+ pages: ['/docs', '/javascript-demo', '/request-demo'],
270
+ };
271
+ }
272
+ \`\`\`
273
+ `;
274
+
275
+ const siteStatusPageSource = `export const config = {
276
+ path: '/api/site-status.json',
277
+ metadata: { title: 'Site Status' },
278
+ menu: false,
279
+ };
280
+
281
+ export default function siteStatusPage() {
282
+ return {
283
+ status: 'ready',
284
+ generatedBy: 'Rocket',
285
+ pages: ['/docs', '/javascript-demo', '/request-demo'],
286
+ };
287
+ }
288
+ `;
289
+
290
+ const STARTER_PAGE_SOURCES = new Map([
291
+ [SHARED_DATA_PATH, sharedDataSource],
292
+ [THEME_CSS_PATH, themeCssSource],
293
+ [INDEX_PAGE_PATH, indexPageSource],
294
+ [DOCS_PAGE_PATH, docsPageSource],
295
+ [JAVASCRIPT_DEMO_PAGE_PATH, javascriptDemoPageSource],
296
+ [REQUEST_DEMO_PAGE_PATH, requestDemoPageSource],
297
+ [SITE_STATUS_PAGE_PATH, siteStatusPageSource],
298
+ ]);
299
+
39
300
  const rocketAgentSkillSource = `---
40
301
  name: rocket
41
302
  description: Use when editing Rocket Pages, config, layouts, component reference Pages, or build behavior in this project.
@@ -43,18 +304,26 @@ description: Use when editing Rocket Pages, config, layouts, component reference
43
304
 
44
305
  # Rocket
45
306
 
46
- Use this skill when working on this project's Rocket site.
47
-
48
307
  ## Rules
49
308
 
50
- - Read \`rocket-config.js\` before changing Pages.
51
- - Rocket discovers Pages from \`includeGlobs\`.
52
- - Every Page owns its public URL through \`config.path\`.
53
- - Put general documentation Pages under \`docs/pages\`.
54
- - Put component reference Pages next to the component they document.
55
- - Prefer Markdown Pages for durable content.
56
- - Use JavaScript Pages only for request-time or programmatic rendering.
57
- - Keep Rocket changes buildable with \`npm run build\`.
309
+ - Read \`rocket-config.js\` first; Page discovery follows \`includeGlobs\`.
310
+ - Every Page owns its URL through \`config.path\`; put general docs in \`docs/pages\` and component docs next to the component.
311
+ - Prefer Markdown for durable content; use JavaScript Pages for request-time or programmatic output.
312
+ - Prefer interactive examples for component and behavior docs; use \`js demo\` when readers benefit from trying the UI.
313
+ - Prefer Atlas docs layouts: \`atlasDocLayout\` for docs, \`atlasHeroLayout\` for a standalone docs home, with matching \`components\` exports.
314
+ - Markdown using Rocket custom elements needs a \`components\` export; use Atlas component maps or \`rocketDemoComponents\`.
315
+ - Add \`menu.iconName\` to Atlas docs navigation Pages so the left navigation has icons.
316
+ - Direct layout re-exports are supported when no local wrapper function is needed.
317
+ - For Atlas theming, use shared layout data with \`stylesheets\` and centralized CSS variables instead of per-Page style injection.
318
+ - To add a general Page, create \`docs/pages/name.rocket.md\`, set \`config.path\`, \`metadata\`, \`menu.iconName\`, and use the shared docs layout.
319
+ - Custom layouts rendering \`rocket-icon\` need \`addBootstrapIconLibrary(pageData)\` before \`document()\`.
320
+ - Static JavaScript Pages render once per concrete path; query/header/cookie/live-data output needs \`render: 'server'\`.
321
+ - Static Request Demos should target concrete non-query URLs.
322
+ - After adding a \`js demo\`, verify the parent Page and Standalone Demo URL \`/page/_demo/demoName/\`.
323
+ - If \`rocket init\` fails because package.json has \`"type": "commonjs"\`, change it to \`"type": "module"\` or rerun \`npx rocket init --yes\`.
324
+ - If dev server watchers fail with \`EMFILE\`, run \`npm start -- --no-watch --no-open\` and use \`Ctrl+R\` for manual restarts.
325
+ - When smoke-testing Pages with curl, send \`Accept: text/html\`: \`curl -H 'Accept: text/html' http://localhost:8888/path\`.
326
+ - Keep \`npm run build\` passing; record Rocket package issues separately from local workarounds.
58
327
  `;
59
328
 
60
329
  export class RocketInit {
@@ -64,18 +333,20 @@ export class RocketInit {
64
333
  async setupCommand(program) {
65
334
  program
66
335
  .command('init')
67
- .description('create a minimal Rocket project shape')
68
- .action(() => {
69
- const result = this.init();
336
+ .description('create a Rocket docs starter')
337
+ .option('-y, --yes', 'update an explicit CommonJS package.json type to module')
338
+ .action(options => {
339
+ const result = this.init({ yes: options.yes });
70
340
  reportInitResult(result);
71
341
  });
72
342
  }
73
343
 
74
344
  /**
345
+ * @param {RocketInitOptions} [options]
75
346
  * @returns {RocketInitResult}
76
347
  */
77
- init() {
78
- const packageJsonUpdate = preparePackageJsonUpdate('package.json');
348
+ init(options = {}) {
349
+ const packageJsonUpdate = preparePackageJsonUpdate('package.json', options);
79
350
  /** @type {RocketInitResult} */
80
351
  const result = {
81
352
  created: [],
@@ -89,14 +360,27 @@ export class RocketInit {
89
360
  writeJsonFile('package.json', packageJsonUpdate.packageJson);
90
361
  }
91
362
 
363
+ const shouldCreateStarterPages =
364
+ existingRocketPageFileCount(process.cwd()) < EXISTING_ROCKET_PAGE_NOISE_THRESHOLD;
365
+
92
366
  writeFileIfMissing(ROCKET_CONFIG_PATH, rocketConfigSource, result);
93
- writeFileIfMissing(INDEX_PAGE_PATH, indexPageSource, result);
367
+ if (shouldCreateStarterPages) {
368
+ for (const filePath of STARTER_PAGE_FILES) {
369
+ writeFileIfMissing(filePath, STARTER_PAGE_SOURCES.get(filePath) || '', result);
370
+ }
371
+ }
94
372
  writeFileIfMissing(ROCKET_AGENT_SKILL_PATH, rocketAgentSkillSource, result);
95
373
 
96
374
  return result;
97
375
  }
98
376
  }
99
377
 
378
+ /**
379
+ * @typedef {{
380
+ * yes?: boolean;
381
+ * }} RocketInitOptions
382
+ */
383
+
100
384
  /**
101
385
  * @typedef {{
102
386
  * created: string[];
@@ -113,6 +397,7 @@ export class RocketInit {
113
397
 
114
398
  /**
115
399
  * @param {string} filePath
400
+ * @param {RocketInitOptions} options
116
401
  * @returns {{
117
402
  * changed: boolean;
118
403
  * packageJson: Record<string, any>;
@@ -121,7 +406,7 @@ export class RocketInit {
121
406
  * nextSteps: string[];
122
407
  * } | undefined}
123
408
  */
124
- function preparePackageJsonUpdate(filePath) {
409
+ function preparePackageJsonUpdate(filePath, options) {
125
410
  if (!existsSync(filePath)) {
126
411
  return undefined;
127
412
  }
@@ -133,19 +418,19 @@ function preparePackageJsonUpdate(filePath) {
133
418
  if (!isPlainRecord(packageJson)) {
134
419
  throw new Error('package.json must contain a JSON object');
135
420
  }
136
- if (packageJson.type === 'commonjs') {
137
- throw new Error(
138
- `Rocket init expects an ESM project. Found package.json type "commonjs". ` +
139
- `Change package.json to "type": "module" before running rocket init.`,
140
- );
141
- }
142
421
 
143
422
  /** @type {string[]} */
144
423
  const updated = [];
145
424
  /** @type {string[]} */
146
425
  const skipped = [];
147
426
 
148
- if (!hasOwn(packageJson, 'type')) {
427
+ if (packageJson.type === 'commonjs') {
428
+ if (!options.yes) {
429
+ throw commonJsProjectError();
430
+ }
431
+ packageJson.type = 'module';
432
+ updated.push('type');
433
+ } else if (!hasOwn(packageJson, 'type')) {
149
434
  packageJson.type = 'module';
150
435
  updated.push('type');
151
436
  } else if (packageJson.type === 'module') {
@@ -188,6 +473,19 @@ function preparePackageJsonUpdate(filePath) {
188
473
  };
189
474
  }
190
475
 
476
+ function commonJsProjectError() {
477
+ return new Error(
478
+ `Rocket init expects an ESM project. Found package.json type "commonjs".\n\n` +
479
+ `Update package.json before running rocket init:\n` +
480
+ `- "type": "commonjs",\n` +
481
+ `+ "type": "module",\n\n` +
482
+ `Then rerun:\n` +
483
+ ` npx rocket init\n\n` +
484
+ `Or let Rocket apply that package.json change:\n` +
485
+ ` npx rocket init --yes`,
486
+ );
487
+ }
488
+
191
489
  /**
192
490
  * @param {Record<string, any>} scripts
193
491
  * @param {{
@@ -204,7 +502,7 @@ function addRocketScript(scripts, { genericName, fallbackName, command, updated,
204
502
  updated.push(`scripts.${genericName}`);
205
503
  return;
206
504
  }
207
- if (scripts[genericName] === command) {
505
+ if (scriptRunsRocketCommand(scripts[genericName], genericName)) {
208
506
  skipped.push(`scripts.${genericName}`);
209
507
  return;
210
508
  }
@@ -237,8 +535,8 @@ function rocketNextSteps(packageJson) {
237
535
  return ['npx rocket start', 'npx rocket build'];
238
536
  }
239
537
  return [
240
- npmScriptCommand(packageJson.scripts, 'start', 'rocket:start', 'rocket start'),
241
- npmScriptCommand(packageJson.scripts, 'build', 'rocket:build', 'rocket build'),
538
+ npmScriptCommand(packageJson.scripts, 'start', 'rocket:start'),
539
+ npmScriptCommand(packageJson.scripts, 'build', 'rocket:build'),
242
540
  ];
243
541
  }
244
542
 
@@ -246,18 +544,85 @@ function rocketNextSteps(packageJson) {
246
544
  * @param {Record<string, any>} scripts
247
545
  * @param {string} genericName
248
546
  * @param {string} fallbackName
249
- * @param {string} command
250
547
  */
251
- function npmScriptCommand(scripts, genericName, fallbackName, command) {
252
- if (scripts[genericName] === command) {
548
+ function npmScriptCommand(scripts, genericName, fallbackName) {
549
+ if (scriptRunsRocketCommand(scripts[genericName], genericName)) {
253
550
  return genericName === 'start' ? 'npm start' : `npm run ${genericName}`;
254
551
  }
255
- if (scripts[fallbackName] === command) {
552
+ if (scriptRunsRocketCommand(scripts[fallbackName], genericName)) {
256
553
  return `npm run ${fallbackName}`;
257
554
  }
258
555
  return `npx rocket ${genericName}`;
259
556
  }
260
557
 
558
+ /**
559
+ * @param {unknown} script
560
+ * @param {string} commandName
561
+ * @returns {boolean}
562
+ */
563
+ function scriptRunsRocketCommand(script, commandName) {
564
+ if (typeof script !== 'string') {
565
+ return false;
566
+ }
567
+ const shellBoundary = '[\\s"\\\'`;&|()]';
568
+ return new RegExp(
569
+ `(^|${shellBoundary})(?:npx\\s+)?rocket\\s+${commandName}(?=$|${shellBoundary})`,
570
+ ).test(script);
571
+ }
572
+
573
+ /**
574
+ * @param {string} directory
575
+ * @returns {number}
576
+ */
577
+ function existingRocketPageFileCount(directory) {
578
+ if (!existsSync(directory)) {
579
+ return 0;
580
+ }
581
+ return countRocketPageFiles(directory, { count: 0 });
582
+ }
583
+
584
+ /**
585
+ * @param {string} directory
586
+ * @param {{ count: number }} state
587
+ * @returns {number}
588
+ */
589
+ function countRocketPageFiles(directory, state) {
590
+ if (state.count >= EXISTING_ROCKET_PAGE_NOISE_THRESHOLD) {
591
+ return state.count;
592
+ }
593
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
594
+ if (shouldIgnoreRocketPageScanEntry(entry.name)) {
595
+ continue;
596
+ }
597
+ const entryPath = path.join(directory, entry.name);
598
+ if (entry.isDirectory()) {
599
+ countRocketPageFiles(entryPath, state);
600
+ } else if (isRocketPageFile(entry.name)) {
601
+ state.count += 1;
602
+ }
603
+ if (state.count >= EXISTING_ROCKET_PAGE_NOISE_THRESHOLD) {
604
+ return state.count;
605
+ }
606
+ }
607
+ return state.count;
608
+ }
609
+
610
+ /**
611
+ * @param {string} name
612
+ * @returns {boolean}
613
+ */
614
+ function shouldIgnoreRocketPageScanEntry(name) {
615
+ return ['.git', '.netlify', 'coverage', 'dist', 'node_modules', 'temp'].includes(name);
616
+ }
617
+
618
+ /**
619
+ * @param {string} fileName
620
+ * @returns {boolean}
621
+ */
622
+ function isRocketPageFile(fileName) {
623
+ return fileName.endsWith('.rocket.md') || fileName.endsWith('.rocket.js');
624
+ }
625
+
261
626
  /**
262
627
  * @param {string} filePath
263
628
  * @param {string} contents