@rocket/js 0.1.1 → 0.2.0
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/README.md +14 -23
- package/dist-types/exports/layouts/atlasBlog.d.ts +2 -0
- package/dist-types/exports/layouts/atlasBlog.d.ts.map +1 -0
- package/dist-types/exports/types/rocket.d.ts +68 -2
- package/dist-types/exports/types/rocket.d.ts.map +1 -1
- package/dist-types/exports/types.d.ts +1 -1
- package/dist-types/exports/types.d.ts.map +1 -1
- package/dist-types/src/PageData.d.ts +3 -1
- package/dist-types/src/PageData.d.ts.map +1 -1
- package/dist-types/src/RocketJsDemo.d.ts.map +1 -1
- package/dist-types/src/SocialPreviewPlayground.d.ts +1 -0
- package/dist-types/src/SocialPreviewPlayground.d.ts.map +1 -1
- package/dist-types/src/asyncMessage.d.ts +5 -0
- package/dist-types/src/asyncMessage.d.ts.map +1 -1
- package/dist-types/src/cli/RocketBuild.d.ts.map +1 -1
- package/dist-types/src/cli/RocketCli.d.ts.map +1 -1
- package/dist-types/src/cli/RocketInit.d.ts +5 -1
- package/dist-types/src/cli/RocketInit.d.ts.map +1 -1
- package/dist-types/src/cli/RocketStart.d.ts +6 -1
- package/dist-types/src/cli/RocketStart.d.ts.map +1 -1
- package/dist-types/src/component-hydration.d.ts.map +1 -1
- package/dist-types/src/components/RocketDrawer.d.ts +8 -0
- package/dist-types/src/components/RocketDrawer.d.ts.map +1 -1
- package/dist-types/src/components/RocketSocialLink.d.ts.map +1 -1
- package/dist-types/src/config.d.ts.map +1 -1
- package/dist-types/src/feeds.d.ts +38 -0
- package/dist-types/src/feeds.d.ts.map +1 -0
- package/dist-types/src/hydration/hydrationLoader.d.ts +13 -4
- package/dist-types/src/hydration/hydrationLoader.d.ts.map +1 -1
- package/dist-types/src/icons.d.ts.map +1 -1
- package/dist-types/src/layouts/atlas/atlasBlogLayout.d.ts +42 -0
- package/dist-types/src/layouts/atlas/atlasBlogLayout.d.ts.map +1 -0
- package/dist-types/src/layouts/atlas/atlasDocLayout.d.ts +4 -0
- package/dist-types/src/layouts/atlas/atlasDocLayout.d.ts.map +1 -1
- package/dist-types/src/layouts/atlas/atlasHeroLayout.d.ts.map +1 -1
- package/dist-types/src/layouts/atlas/atlasNotFoundLayout.d.ts.map +1 -1
- package/dist-types/src/layouts/layout-helper.d.ts +3 -3
- package/dist-types/src/layouts/layout-helper.d.ts.map +1 -1
- package/dist-types/src/markdownHook.d.ts.map +1 -1
- package/dist-types/src/menu.d.ts +8 -3
- package/dist-types/src/menu.d.ts.map +1 -1
- package/dist-types/src/menus/MainMenu.d.ts.map +1 -1
- package/dist-types/src/{development-page-module-loader.d.ts → page-module-loader.d.ts} +7 -2
- package/dist-types/src/page-module-loader.d.ts.map +1 -0
- package/dist-types/src/page-runtime.d.ts +11 -0
- package/dist-types/src/page-runtime.d.ts.map +1 -1
- package/dist-types/src/pages.d.ts.map +1 -1
- package/dist-types/src/route-pattern.d.ts +10 -0
- package/dist-types/src/route-pattern.d.ts.map +1 -0
- package/dist-types/src/siteDiscoverability.d.ts +4 -0
- package/dist-types/src/siteDiscoverability.d.ts.map +1 -1
- package/dist-types/src/socialPreviewImages.d.ts +2 -1
- package/dist-types/src/socialPreviewImages.d.ts.map +1 -1
- package/dist-types/src/static-params.d.ts +33 -0
- package/dist-types/src/static-params.d.ts.map +1 -0
- package/dist-types/src/transform.d.ts +4 -0
- package/dist-types/src/transform.d.ts.map +1 -1
- package/dist-types/src/wds-plugin.d.ts.map +1 -1
- package/exports/layouts/atlasBlog.js +11 -0
- package/exports/types/rocket.test.ts +26 -0
- package/exports/types/rocket.ts +78 -2
- package/exports/types.ts +7 -0
- package/package.json +16 -32
- package/src/PageData.js +12 -2
- package/src/RocketJsDemo.js +1 -0
- package/src/SocialPreviewPlayground.js +26 -20
- package/src/asyncMessage.js +8 -2
- package/src/cli/RocketBuild.js +110 -13
- package/src/cli/RocketCli.js +11 -1
- package/src/cli/RocketInit.js +64 -12
- package/src/cli/RocketStart.js +60 -16
- package/src/cli/cli.js +10 -1
- package/src/component-hydration.js +23 -3
- package/src/components/RocketDrawer.js +44 -10
- package/src/components/RocketSocialLink.js +26 -2
- package/src/config.js +33 -4
- package/src/feeds.js +212 -0
- package/src/hydration/hydrationLoader.js +80 -48
- package/src/icons.js +5 -0
- package/src/layouts/atlas/atlasBlog.css +143 -0
- package/src/layouts/atlas/atlasBlogLayout.js +234 -0
- package/src/layouts/atlas/atlasDocLayout.js +10 -1
- package/src/layouts/atlas/atlasHeroLayout.js +10 -6
- package/src/layouts/atlas/atlasNotFoundLayout.js +12 -22
- package/src/layouts/layout-helper.js +5 -5
- package/src/layouts/layout.js +2 -2
- package/src/loaded-page-module.js +3 -1
- package/src/markdownCompiler.js +2 -2
- package/src/markdownHook.js +7 -2
- package/src/menu.js +25 -19
- package/src/menus/MainMenu.js +10 -4
- package/src/{static-page-module-loader.js → page-module-loader.js} +6 -1
- package/src/page-pagination.js +2 -2
- package/src/page-runtime.js +48 -3
- package/src/pages.js +47 -56
- package/src/publicAssets.js +2 -2
- package/src/route-pattern.js +21 -0
- package/src/siteDiscoverability.js +14 -1
- package/src/socialPreviewImages.js +80 -14
- package/src/standalone-demo-url.js +2 -2
- package/src/static-params.js +114 -0
- package/src/transform.js +89 -22
- package/src/wds-plugin.js +16 -3
- package/dist-types/src/development-page-module-loader.d.ts.map +0 -1
- package/dist-types/src/static-page-module-loader.d.ts +0 -15
- package/dist-types/src/static-page-module-loader.d.ts.map +0 -1
- package/src/development-page-module-loader.js +0 -29
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
DEFAULT_SOCIAL_PREVIEW_IMAGE_WIDTH,
|
|
4
4
|
defaultSocialPreviewTemplate,
|
|
5
5
|
} from './defaultSocialPreviewTemplate.js';
|
|
6
|
+
import { debounce } from './debounce.js';
|
|
6
7
|
|
|
7
8
|
const DEFAULT_TITLE = 'Runtime Guide';
|
|
8
9
|
const DEFAULT_DESCRIPTION = 'Learn how Acme UI components load, render, and ship with Rocket.';
|
|
@@ -81,22 +82,23 @@ export class SocialPreviewPlayground extends HTMLElement {
|
|
|
81
82
|
this.templateFrame = null;
|
|
82
83
|
/** @type {HTMLAnchorElement | null} */
|
|
83
84
|
this.pngLink = null;
|
|
85
|
+
// Typing reloads the preview iframe and rewrites history, so batch keystrokes.
|
|
86
|
+
this.schedulePreviewUpdate = debounce(() => this.updatePreviewResources(), 150);
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
connectedCallback() {
|
|
87
|
-
if (this.shadowRoot) {
|
|
88
|
-
|
|
90
|
+
if (!this.shadowRoot) {
|
|
91
|
+
this.config = this.readConfig();
|
|
92
|
+
this.state = this.initialState(this.config);
|
|
93
|
+
this.attachShadow({ mode: 'open' });
|
|
94
|
+
this.render();
|
|
95
|
+
this.connectControls();
|
|
96
|
+
this.updatePreviewResources();
|
|
89
97
|
}
|
|
90
|
-
this.config = this.readConfig();
|
|
91
|
-
this.state = this.initialState(this.config);
|
|
92
|
-
const shadowRoot = this.attachShadow({ mode: 'open' });
|
|
93
|
-
this.render();
|
|
94
|
-
this.connectControls();
|
|
95
|
-
this.updatePreviewResources();
|
|
96
98
|
this.resizePreviewFrame();
|
|
97
|
-
const frame = shadowRoot
|
|
99
|
+
const frame = this.shadowRoot?.querySelector('[data-social-preview-frame-shell]');
|
|
98
100
|
if (frame && 'ResizeObserver' in window) {
|
|
99
|
-
this.resizeObserver
|
|
101
|
+
this.resizeObserver ??= new ResizeObserver(() => this.resizePreviewFrame());
|
|
100
102
|
this.resizeObserver.observe(frame);
|
|
101
103
|
}
|
|
102
104
|
}
|
|
@@ -160,8 +162,8 @@ export class SocialPreviewPlayground extends HTMLElement {
|
|
|
160
162
|
const page = config.pages.find(candidate => candidate.path === config.selectedPath);
|
|
161
163
|
return {
|
|
162
164
|
selectedPath: page?.path ?? config.selectedPath,
|
|
163
|
-
title:
|
|
164
|
-
description:
|
|
165
|
+
title: page?.title || config.defaultTitle,
|
|
166
|
+
description: page?.description || config.defaultDescription,
|
|
165
167
|
};
|
|
166
168
|
}
|
|
167
169
|
|
|
@@ -423,11 +425,11 @@ export class SocialPreviewPlayground extends HTMLElement {
|
|
|
423
425
|
});
|
|
424
426
|
titleInput?.addEventListener('input', () => {
|
|
425
427
|
this.state.title = titleInput.value;
|
|
426
|
-
this.
|
|
428
|
+
this.schedulePreviewUpdate();
|
|
427
429
|
});
|
|
428
430
|
descriptionInput?.addEventListener('input', () => {
|
|
429
431
|
this.state.description = descriptionInput.value;
|
|
430
|
-
this.
|
|
432
|
+
this.schedulePreviewUpdate();
|
|
431
433
|
});
|
|
432
434
|
shadowRoot
|
|
433
435
|
.querySelector('button[data-social-preview-previous-page]')
|
|
@@ -506,7 +508,11 @@ export class SocialPreviewPlayground extends HTMLElement {
|
|
|
506
508
|
return;
|
|
507
509
|
}
|
|
508
510
|
if (this.config.templateUrl) {
|
|
509
|
-
|
|
511
|
+
const templateSrc = this.socialPreviewResourcePath(this.config.templateUrl);
|
|
512
|
+
// assigning an unchanged src still reloads the iframe, so compare first
|
|
513
|
+
if (this.templateFrame.getAttribute('src') !== templateSrc) {
|
|
514
|
+
this.templateFrame.src = templateSrc;
|
|
515
|
+
}
|
|
510
516
|
} else {
|
|
511
517
|
this.templateFrame.srcdoc = this.previewHtml();
|
|
512
518
|
}
|
|
@@ -515,11 +521,11 @@ export class SocialPreviewPlayground extends HTMLElement {
|
|
|
515
521
|
this.pngLink.download = this.selectedPage()?.downloadFilename ?? 'social-preview.png';
|
|
516
522
|
}
|
|
517
523
|
if (this.config.syncHistory && this.config.workflowUrl) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
524
|
+
const workflowPath = this.socialPreviewResourcePath(this.config.workflowUrl);
|
|
525
|
+
const currentPath = `${window.location.pathname}${window.location.search}`;
|
|
526
|
+
if (workflowPath !== currentPath) {
|
|
527
|
+
window.history.replaceState(null, '', workflowPath);
|
|
528
|
+
}
|
|
523
529
|
}
|
|
524
530
|
}
|
|
525
531
|
|
package/src/asyncMessage.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Turns a MessagePort into an AsyncPort, which can wait for responses.
|
|
6
|
+
*
|
|
7
|
+
* Do not use sendAndWait from inside module customization hooks: the main
|
|
8
|
+
* thread may be blocked in Atomics.wait (e.g. during a nested
|
|
9
|
+
* module.register() call) and unable to reply, which deadlocks the hook.
|
|
10
|
+
*
|
|
6
11
|
* @param {MessagePort} port
|
|
7
12
|
* @returns {AsyncPort}
|
|
8
13
|
*/
|
|
@@ -13,9 +18,10 @@ export function makeAsyncPort(port) {
|
|
|
13
18
|
p._wait.resolve(message);
|
|
14
19
|
p._wait = Promise.withResolvers();
|
|
15
20
|
});
|
|
16
|
-
p.sendAndWait =
|
|
21
|
+
p.sendAndWait = message => {
|
|
22
|
+
const wait = p._wait;
|
|
17
23
|
p.postMessage(message);
|
|
18
|
-
return
|
|
24
|
+
return wait.promise;
|
|
19
25
|
};
|
|
20
26
|
return p;
|
|
21
27
|
}
|
package/src/cli/RocketBuild.js
CHANGED
|
@@ -6,8 +6,10 @@ import { build } from 'vite';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { PageRuntime } from '../page-runtime.js';
|
|
8
8
|
import { createIconAssetStore, rocketIconRuntimeOutputs } from '../icons.js';
|
|
9
|
-
import { paginatedArchivePaths } from '../page-pagination.js';
|
|
10
|
-
import {
|
|
9
|
+
import { hasPagePagination, paginatedArchivePaths } from '../page-pagination.js';
|
|
10
|
+
import { hasPageFeed, pageFeedPath } from '../feeds.js';
|
|
11
|
+
import { hasStaticParams, staticParamsPagePaths } from '../static-params.js';
|
|
12
|
+
import { createPageModuleLoader } from '../page-module-loader.js';
|
|
11
13
|
import { writeSiteDiscoverabilityOutputs } from '../siteDiscoverability.js';
|
|
12
14
|
import { normalizeDocumentPath, standaloneDemoPaths } from '../standalone-demo-url.js';
|
|
13
15
|
import {
|
|
@@ -46,6 +48,7 @@ export class RocketBuild {
|
|
|
46
48
|
|
|
47
49
|
program
|
|
48
50
|
.command('build')
|
|
51
|
+
.description('build the static site into the output directory')
|
|
49
52
|
.option('-o, --output-dir <path>', 'Path where to output built files')
|
|
50
53
|
.action(async ({ outputDir }) => {
|
|
51
54
|
if (outputDir) {
|
|
@@ -96,6 +99,8 @@ export class RocketBuild {
|
|
|
96
99
|
emitRedirectFallbacks: !this.cli.config.adapter,
|
|
97
100
|
});
|
|
98
101
|
const outDir = this.outputDir ? path.resolve(this.outputDir) : path.resolve('dist');
|
|
102
|
+
// Vite's emptyOutDir force-clears any outDir, so containment must hold on every path.
|
|
103
|
+
assertOutputDirInsideProject(outDir);
|
|
99
104
|
assertPublicAssetsOutputDir({ projectRoot: process.cwd(), outDir });
|
|
100
105
|
|
|
101
106
|
try {
|
|
@@ -217,7 +222,7 @@ export function splitPages(pages) {
|
|
|
217
222
|
export async function renderStaticPages({
|
|
218
223
|
pages,
|
|
219
224
|
staticPages,
|
|
220
|
-
pageModuleLoader =
|
|
225
|
+
pageModuleLoader = createPageModuleLoader(),
|
|
221
226
|
origin = 'http://localhost',
|
|
222
227
|
urlLifecycle,
|
|
223
228
|
siteHeadMetadata,
|
|
@@ -232,6 +237,8 @@ export async function renderStaticPages({
|
|
|
232
237
|
assertStaticJavaScriptPagesHaveConcretePaths(staticPages);
|
|
233
238
|
assertNoStandaloneDemoPathCollisions(pages, staticPages);
|
|
234
239
|
assertNoPaginatedArchivePathCollisions(pages, staticPages);
|
|
240
|
+
assertPageFeedsAreBuildable({ pages, staticPages, siteOrigin });
|
|
241
|
+
assertNoStaticParamsPathCollisions(pages, staticPages);
|
|
235
242
|
validateUrlLifecycleGeneratedOutputCollisions({
|
|
236
243
|
redirects: urlLifecycle?.redirects,
|
|
237
244
|
pages,
|
|
@@ -265,6 +272,13 @@ export async function renderStaticPages({
|
|
|
265
272
|
});
|
|
266
273
|
for (const [pagePath, page] of staticPages) {
|
|
267
274
|
try {
|
|
275
|
+
if (hasStaticParams(page)) {
|
|
276
|
+
for (const paramPath of staticParamsPagePaths({ pages, page, pagePath })) {
|
|
277
|
+
const paramResponse = await pageRuntime.render(new Request(new URL(paramPath, origin)));
|
|
278
|
+
await writeStaticResponse(paramPath, paramResponse);
|
|
279
|
+
}
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
268
282
|
const response = await pageRuntime.render(new Request(new URL(pagePath, origin)));
|
|
269
283
|
await writeStaticResponse(pagePath, response);
|
|
270
284
|
for (const archivePath of paginatedArchivePaths({ pages, page, pagePath })) {
|
|
@@ -275,6 +289,11 @@ export async function renderStaticPages({
|
|
|
275
289
|
const demoResponse = await pageRuntime.render(new Request(new URL(demoPath, origin)));
|
|
276
290
|
await writeStandaloneDemoResponse(demoPath, page, demoResponse);
|
|
277
291
|
}
|
|
292
|
+
if (hasPageFeed(page)) {
|
|
293
|
+
const feedPath = pageFeedPath(pagePath);
|
|
294
|
+
const feedResponse = await pageRuntime.render(new Request(new URL(feedPath, origin)));
|
|
295
|
+
await writeStaticResponse(feedPath, feedResponse);
|
|
296
|
+
}
|
|
278
297
|
} catch (error) {
|
|
279
298
|
throw new Error(`Failed to render page: ${pagePath}. ${errorMessage(error)}`, {
|
|
280
299
|
cause: error,
|
|
@@ -293,18 +312,55 @@ export async function renderStaticPages({
|
|
|
293
312
|
*/
|
|
294
313
|
function assertStaticJavaScriptPagesHaveConcretePaths(staticPages) {
|
|
295
314
|
for (const [pagePath, page] of staticPages) {
|
|
296
|
-
if (!page.file.endsWith('.js') || !hasPathParameter(pagePath)) {
|
|
315
|
+
if (!page.file.endsWith('.js') || !hasPathParameter(pagePath) || hasStaticParams(page)) {
|
|
297
316
|
continue;
|
|
298
317
|
}
|
|
299
318
|
throw new Error(
|
|
300
319
|
`Static JavaScript Page ${pagePath} (${page.file}) cannot be rendered as one static ` +
|
|
301
|
-
`output document because its path is parameterized.
|
|
302
|
-
`
|
|
303
|
-
`render: 'server' for this Page today.`,
|
|
320
|
+
`output document because its path is parameterized. Export static params to enumerate ` +
|
|
321
|
+
`its output documents, or use render: 'server' for request-time rendering.`,
|
|
304
322
|
);
|
|
305
323
|
}
|
|
306
324
|
}
|
|
307
325
|
|
|
326
|
+
/**
|
|
327
|
+
* @param {import('@rocket/js/types.js').PageRegistry} pages
|
|
328
|
+
* @param {import('@rocket/js/types.js').PageRegistry} staticPages
|
|
329
|
+
*/
|
|
330
|
+
function assertNoStaticParamsPathCollisions(pages, staticPages) {
|
|
331
|
+
const configuredPages = configuredPageOutputPaths(pages);
|
|
332
|
+
const staticParamsPages = new Map();
|
|
333
|
+
for (const [pagePath, page] of staticPages) {
|
|
334
|
+
if (!hasStaticParams(page)) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (hasPagePagination(page)) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
`Static JavaScript Page ${pagePath} (${page.file}) cannot combine static params with ` +
|
|
340
|
+
`pagination. Paginate a concrete archive Page instead.`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
for (const paramPath of staticParamsPagePaths({ pages, page, pagePath })) {
|
|
344
|
+
const outputPath = normalizeDocumentPath(paramPath);
|
|
345
|
+
const collision = configuredPages.get(outputPath);
|
|
346
|
+
if (collision) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`Static params output path ${paramPath} for page ${page.file} collides with ` +
|
|
349
|
+
`configured Page ${collision.page.file} at ${collision.path}`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
const paramCollision = staticParamsPages.get(outputPath);
|
|
353
|
+
if (paramCollision) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`Static params output path ${paramPath} for page ${page.file} collides with ` +
|
|
356
|
+
`static params output path ${paramCollision.path} for page ${paramCollision.page.file}`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
staticParamsPages.set(outputPath, { path: paramPath, page });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
308
364
|
/**
|
|
309
365
|
* @param {string} pagePath
|
|
310
366
|
*/
|
|
@@ -312,6 +368,40 @@ function hasPathParameter(pagePath) {
|
|
|
312
368
|
return /(^|\/):[^/]+/.test(pagePath);
|
|
313
369
|
}
|
|
314
370
|
|
|
371
|
+
/**
|
|
372
|
+
* @param {{
|
|
373
|
+
* pages: import('@rocket/js/types.js').PageRegistry;
|
|
374
|
+
* staticPages: import('@rocket/js/types.js').PageRegistry;
|
|
375
|
+
* siteOrigin?: string;
|
|
376
|
+
* }} options
|
|
377
|
+
*/
|
|
378
|
+
function assertPageFeedsAreBuildable({ pages, staticPages, siteOrigin }) {
|
|
379
|
+
for (const [pagePath, page] of staticPages) {
|
|
380
|
+
if (!hasPageFeed(page)) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (hasPathParameter(pagePath)) {
|
|
384
|
+
throw new Error(
|
|
385
|
+
`Page Feed for ${page.file} cannot use the parameterized path ${pagePath}. ` +
|
|
386
|
+
`Page Feeds need a concrete owning Page path.`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
if (typeof siteOrigin !== 'string' || siteOrigin.trim() === '') {
|
|
390
|
+
throw new Error(
|
|
391
|
+
`Page Feed ${pageFeedPath(pagePath)} for ${page.file} requires a Site Origin for ` +
|
|
392
|
+
`absolute feed URLs. Add siteOrigin: 'https://example.com' to rocket-config.js.`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
const collision = pages.get(pageFeedPath(pagePath));
|
|
396
|
+
if (collision) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`Page Feed ${pageFeedPath(pagePath)} for ${page.file} collides with configured ` +
|
|
399
|
+
`Page ${collision.file}`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
315
405
|
/**
|
|
316
406
|
* @param {import('@rocket/js/types.js').PageRegistry} pages
|
|
317
407
|
* @param {import('@rocket/js/types.js').PageRegistry} staticPages
|
|
@@ -429,23 +519,30 @@ function writeIconAssetOutputs(iconAssetStore) {
|
|
|
429
519
|
/**
|
|
430
520
|
* @param {string} outDir
|
|
431
521
|
*/
|
|
432
|
-
function
|
|
433
|
-
if (!existsSync(outDir)) {
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
522
|
+
function assertOutputDirInsideProject(outDir) {
|
|
436
523
|
const projectRoot = path.resolve(process.cwd());
|
|
437
524
|
const relativeOutDir = path.relative(projectRoot, outDir);
|
|
438
525
|
if (
|
|
439
526
|
relativeOutDir === '' ||
|
|
440
|
-
relativeOutDir.startsWith(`..${path.sep}`) ||
|
|
441
527
|
relativeOutDir === '..' ||
|
|
528
|
+
relativeOutDir.startsWith(`..${path.sep}`) ||
|
|
442
529
|
path.isAbsolute(relativeOutDir)
|
|
443
530
|
) {
|
|
444
531
|
throw new Error(
|
|
445
532
|
`Invalid build output directory ${outDir}. ` +
|
|
446
|
-
`Rocket
|
|
533
|
+
`Rocket only writes build output inside the project root.`,
|
|
447
534
|
);
|
|
448
535
|
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* @param {string} outDir
|
|
540
|
+
*/
|
|
541
|
+
function clearExistingOutputDir(outDir) {
|
|
542
|
+
if (!existsSync(outDir)) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
assertOutputDirInsideProject(outDir);
|
|
449
546
|
rmSync(outDir, { recursive: true });
|
|
450
547
|
}
|
|
451
548
|
|
package/src/cli/RocketCli.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
1
2
|
import { Command } from 'commander';
|
|
2
3
|
import { readConfig } from '../config.js';
|
|
3
4
|
import { RocketBuild } from './RocketBuild.js';
|
|
4
5
|
import { RocketInit } from './RocketInit.js';
|
|
5
6
|
import { RocketStart } from './RocketStart.js';
|
|
6
7
|
|
|
8
|
+
const packageJson = JSON.parse(
|
|
9
|
+
readFileSync(new URL('../../package.json', import.meta.url), 'utf8'),
|
|
10
|
+
);
|
|
11
|
+
|
|
7
12
|
export class RocketCli {
|
|
8
13
|
/** @type {string[]} */
|
|
9
14
|
argv;
|
|
@@ -21,7 +26,12 @@ export class RocketCli {
|
|
|
21
26
|
this.argv = argv;
|
|
22
27
|
|
|
23
28
|
this.program = new Command();
|
|
24
|
-
this.program
|
|
29
|
+
this.program
|
|
30
|
+
.name('rocket')
|
|
31
|
+
.description('HTML-first static-site metaframework for content sites and Web Component docs')
|
|
32
|
+
.version(packageJson.version)
|
|
33
|
+
.allowUnknownOption(true)
|
|
34
|
+
.option('-c, --config-file <path>', 'path to config file');
|
|
25
35
|
this.program.parseOptions(this.argv);
|
|
26
36
|
|
|
27
37
|
this.program.allowUnknownOption(false);
|
package/src/cli/RocketInit.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
const ROCKET_CONFIG_PATH = 'rocket-config.js';
|
|
6
6
|
const INDEX_PAGE_PATH = 'docs/pages/index.rocket.md';
|
|
7
7
|
const SHARED_DATA_PATH = 'docs/pages/sharedData.js';
|
|
8
|
+
const THEME_CSS_PATH = 'public/rocket-theme.css';
|
|
8
9
|
const DOCS_PAGE_PATH = 'docs/pages/docs.rocket.md';
|
|
9
10
|
const JAVASCRIPT_DEMO_PAGE_PATH = 'docs/pages/javascript-demo.rocket.md';
|
|
10
11
|
const REQUEST_DEMO_PAGE_PATH = 'docs/pages/request-demo.rocket.md';
|
|
@@ -13,6 +14,7 @@ const ROCKET_AGENT_SKILL_PATH = '.agents/skills/rocket/SKILL.md';
|
|
|
13
14
|
const EXISTING_ROCKET_PAGE_NOISE_THRESHOLD = 3;
|
|
14
15
|
const STARTER_PAGE_FILES = [
|
|
15
16
|
SHARED_DATA_PATH,
|
|
17
|
+
THEME_CSS_PATH,
|
|
16
18
|
INDEX_PAGE_PATH,
|
|
17
19
|
DOCS_PAGE_PATH,
|
|
18
20
|
JAVASCRIPT_DEMO_PAGE_PATH,
|
|
@@ -28,6 +30,8 @@ export default {
|
|
|
28
30
|
|
|
29
31
|
const sharedDataSource = `import { resolve } from '@rocket/js/resolve.js';
|
|
30
32
|
|
|
33
|
+
const themeStylesheets = ['/rocket-theme.css'];
|
|
34
|
+
|
|
31
35
|
export const headerData = {
|
|
32
36
|
logo: [
|
|
33
37
|
resolve('@rocket/js/docs/assets/rocket-logo-light.svg', import.meta),
|
|
@@ -47,6 +51,7 @@ export const footerData = [];
|
|
|
47
51
|
export const heroData = {
|
|
48
52
|
headerData,
|
|
49
53
|
footerData,
|
|
54
|
+
stylesheets: themeStylesheets,
|
|
50
55
|
heroMainData: {
|
|
51
56
|
logoNoText: resolve('@rocket/js/docs/assets/rocket-logo-light.svg', import.meta),
|
|
52
57
|
eyebrow: 'ROCKET STARTER',
|
|
@@ -102,10 +107,29 @@ export const heroData = {
|
|
|
102
107
|
export const docData = {
|
|
103
108
|
headerData,
|
|
104
109
|
footerData,
|
|
110
|
+
stylesheets: themeStylesheets,
|
|
105
111
|
navigationIconServerBudget: 35,
|
|
106
112
|
};
|
|
107
113
|
`;
|
|
108
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
|
+
|
|
109
133
|
const indexPageSource = `\`\`\`js server
|
|
110
134
|
export const config = {
|
|
111
135
|
path: '/',
|
|
@@ -265,6 +289,7 @@ export default function siteStatusPage() {
|
|
|
265
289
|
|
|
266
290
|
const STARTER_PAGE_SOURCES = new Map([
|
|
267
291
|
[SHARED_DATA_PATH, sharedDataSource],
|
|
292
|
+
[THEME_CSS_PATH, themeCssSource],
|
|
268
293
|
[INDEX_PAGE_PATH, indexPageSource],
|
|
269
294
|
[DOCS_PAGE_PATH, docsPageSource],
|
|
270
295
|
[JAVASCRIPT_DEMO_PAGE_PATH, javascriptDemoPageSource],
|
|
@@ -289,10 +314,15 @@ description: Use when editing Rocket Pages, config, layouts, component reference
|
|
|
289
314
|
- Markdown using Rocket custom elements needs a \`components\` export; use Atlas component maps or \`rocketDemoComponents\`.
|
|
290
315
|
- Add \`menu.iconName\` to Atlas docs navigation Pages so the left navigation has icons.
|
|
291
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.
|
|
292
319
|
- Custom layouts rendering \`rocket-icon\` need \`addBootstrapIconLibrary(pageData)\` before \`document()\`.
|
|
293
320
|
- Static JavaScript Pages render once per concrete path; query/header/cookie/live-data output needs \`render: 'server'\`.
|
|
294
321
|
- Static Request Demos should target concrete non-query URLs.
|
|
295
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\`.
|
|
296
326
|
- Keep \`npm run build\` passing; record Rocket package issues separately from local workarounds.
|
|
297
327
|
`;
|
|
298
328
|
|
|
@@ -304,17 +334,19 @@ export class RocketInit {
|
|
|
304
334
|
program
|
|
305
335
|
.command('init')
|
|
306
336
|
.description('create a Rocket docs starter')
|
|
307
|
-
.
|
|
308
|
-
|
|
337
|
+
.option('-y, --yes', 'update an explicit CommonJS package.json type to module')
|
|
338
|
+
.action(options => {
|
|
339
|
+
const result = this.init({ yes: options.yes });
|
|
309
340
|
reportInitResult(result);
|
|
310
341
|
});
|
|
311
342
|
}
|
|
312
343
|
|
|
313
344
|
/**
|
|
345
|
+
* @param {RocketInitOptions} [options]
|
|
314
346
|
* @returns {RocketInitResult}
|
|
315
347
|
*/
|
|
316
|
-
init() {
|
|
317
|
-
const packageJsonUpdate = preparePackageJsonUpdate('package.json');
|
|
348
|
+
init(options = {}) {
|
|
349
|
+
const packageJsonUpdate = preparePackageJsonUpdate('package.json', options);
|
|
318
350
|
/** @type {RocketInitResult} */
|
|
319
351
|
const result = {
|
|
320
352
|
created: [],
|
|
@@ -343,6 +375,12 @@ export class RocketInit {
|
|
|
343
375
|
}
|
|
344
376
|
}
|
|
345
377
|
|
|
378
|
+
/**
|
|
379
|
+
* @typedef {{
|
|
380
|
+
* yes?: boolean;
|
|
381
|
+
* }} RocketInitOptions
|
|
382
|
+
*/
|
|
383
|
+
|
|
346
384
|
/**
|
|
347
385
|
* @typedef {{
|
|
348
386
|
* created: string[];
|
|
@@ -359,6 +397,7 @@ export class RocketInit {
|
|
|
359
397
|
|
|
360
398
|
/**
|
|
361
399
|
* @param {string} filePath
|
|
400
|
+
* @param {RocketInitOptions} options
|
|
362
401
|
* @returns {{
|
|
363
402
|
* changed: boolean;
|
|
364
403
|
* packageJson: Record<string, any>;
|
|
@@ -367,7 +406,7 @@ export class RocketInit {
|
|
|
367
406
|
* nextSteps: string[];
|
|
368
407
|
* } | undefined}
|
|
369
408
|
*/
|
|
370
|
-
function preparePackageJsonUpdate(filePath) {
|
|
409
|
+
function preparePackageJsonUpdate(filePath, options) {
|
|
371
410
|
if (!existsSync(filePath)) {
|
|
372
411
|
return undefined;
|
|
373
412
|
}
|
|
@@ -379,19 +418,19 @@ function preparePackageJsonUpdate(filePath) {
|
|
|
379
418
|
if (!isPlainRecord(packageJson)) {
|
|
380
419
|
throw new Error('package.json must contain a JSON object');
|
|
381
420
|
}
|
|
382
|
-
if (packageJson.type === 'commonjs') {
|
|
383
|
-
throw new Error(
|
|
384
|
-
`Rocket init expects an ESM project. Found package.json type "commonjs". ` +
|
|
385
|
-
`Change package.json to "type": "module" before running rocket init.`,
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
421
|
|
|
389
422
|
/** @type {string[]} */
|
|
390
423
|
const updated = [];
|
|
391
424
|
/** @type {string[]} */
|
|
392
425
|
const skipped = [];
|
|
393
426
|
|
|
394
|
-
if (
|
|
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')) {
|
|
395
434
|
packageJson.type = 'module';
|
|
396
435
|
updated.push('type');
|
|
397
436
|
} else if (packageJson.type === 'module') {
|
|
@@ -434,6 +473,19 @@ function preparePackageJsonUpdate(filePath) {
|
|
|
434
473
|
};
|
|
435
474
|
}
|
|
436
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
|
+
|
|
437
489
|
/**
|
|
438
490
|
* @param {Record<string, any>} scripts
|
|
439
491
|
* @param {{
|
package/src/cli/RocketStart.js
CHANGED
|
@@ -8,6 +8,9 @@ export class RocketStart {
|
|
|
8
8
|
/** @type {RocketStartOptions} */
|
|
9
9
|
startOptions = {};
|
|
10
10
|
|
|
11
|
+
/** @type {import('child_process').ChildProcess | undefined} */
|
|
12
|
+
server = undefined;
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
15
|
* @param {import('commander').Command} program
|
|
13
16
|
* @param {import('./RocketCli.js').RocketCli} cli
|
|
@@ -17,6 +20,7 @@ export class RocketStart {
|
|
|
17
20
|
|
|
18
21
|
program
|
|
19
22
|
.command('start')
|
|
23
|
+
.description('start the development server')
|
|
20
24
|
.option('-p, --port <port>', 'port for the development server')
|
|
21
25
|
.option('--no-open', 'do not open the browser')
|
|
22
26
|
.option('--no-watch', 'disable automatic file watching and reloads')
|
|
@@ -32,7 +36,7 @@ export class RocketStart {
|
|
|
32
36
|
*/
|
|
33
37
|
spawnServer(options = {}) {
|
|
34
38
|
process.stdin.setRawMode?.(true);
|
|
35
|
-
|
|
39
|
+
const server = spawn(
|
|
36
40
|
'node',
|
|
37
41
|
startServerArgs({
|
|
38
42
|
configFilePath: this.cli?.configFilePath,
|
|
@@ -43,13 +47,47 @@ export class RocketStart {
|
|
|
43
47
|
cwd: process.cwd(),
|
|
44
48
|
},
|
|
45
49
|
);
|
|
50
|
+
server.on('exit', (code, signal) => {
|
|
51
|
+
// ignore exits of already-replaced servers during a restart
|
|
52
|
+
if (this.server !== server) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.server = undefined;
|
|
56
|
+
if (this.startOptions.watch !== false) {
|
|
57
|
+
console.log(
|
|
58
|
+
`Rocket dev server stopped (${signal ?? `code ${code}`}). ` +
|
|
59
|
+
'Waiting for changes - press Ctrl+R to restart or Ctrl+C to quit.',
|
|
60
|
+
);
|
|
61
|
+
} else {
|
|
62
|
+
this.shutdown(typeof code === 'number' ? code : 1);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return server;
|
|
46
66
|
}
|
|
47
67
|
|
|
48
68
|
restartServer() {
|
|
49
|
-
this.server
|
|
50
|
-
this.server =
|
|
69
|
+
const oldServer = this.server;
|
|
70
|
+
this.server = undefined;
|
|
51
71
|
console.clear();
|
|
52
72
|
console.log('Restarting rocket server...');
|
|
73
|
+
if (oldServer && oldServer.exitCode === null && oldServer.signalCode === null) {
|
|
74
|
+
// wait for the old process to release its port before respawning
|
|
75
|
+
oldServer.once('exit', () => {
|
|
76
|
+
this.server = this.spawnServer(this.startOptions);
|
|
77
|
+
});
|
|
78
|
+
oldServer.kill();
|
|
79
|
+
} else {
|
|
80
|
+
this.server = this.spawnServer(this.startOptions);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {number} code
|
|
86
|
+
*/
|
|
87
|
+
shutdown(code) {
|
|
88
|
+
process.stdin.setRawMode?.(false);
|
|
89
|
+
this.server?.kill();
|
|
90
|
+
process.exit(code);
|
|
53
91
|
}
|
|
54
92
|
|
|
55
93
|
/**
|
|
@@ -71,25 +109,31 @@ export class RocketStart {
|
|
|
71
109
|
}, 100),
|
|
72
110
|
);
|
|
73
111
|
|
|
74
|
-
// watch
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
112
|
+
// watch the framework source when running from a Rocket checkout
|
|
113
|
+
const frameworkSrcDir = path.join(import.meta.dirname, '..');
|
|
114
|
+
if (!frameworkSrcDir.includes('node_modules') && frameworkSrcDir.startsWith(process.cwd())) {
|
|
115
|
+
watch(
|
|
116
|
+
frameworkSrcDir,
|
|
117
|
+
{ recursive: true },
|
|
118
|
+
debounce(async (_, filename) => {
|
|
119
|
+
if (!filename) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.restartServer();
|
|
123
|
+
}, 100),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
129
|
+
process.on(signal, () => this.shutdown(0));
|
|
85
130
|
}
|
|
86
131
|
|
|
87
132
|
process.stdin.on('data', data => {
|
|
88
133
|
const char = data.toString();
|
|
89
134
|
if (char === '\u0003' || char === '\u0004') {
|
|
90
135
|
// Ctrl+C or Ctrl+D
|
|
91
|
-
this.
|
|
92
|
-
process.exit(0);
|
|
136
|
+
this.shutdown(0);
|
|
93
137
|
} else if (char === '\u0012') {
|
|
94
138
|
// Ctrl+R
|
|
95
139
|
this.restartServer();
|
package/src/cli/cli.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
2
3
|
|
|
3
4
|
import { RocketCli } from './RocketCli.js';
|
|
4
5
|
|
|
5
6
|
const cli = new RocketCli();
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
try {
|
|
9
|
+
await cli.start();
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error(error instanceof Error ? error.message : error);
|
|
12
|
+
if (process.env.ROCKET_DEBUG) {
|
|
13
|
+
console.error(error);
|
|
14
|
+
}
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|