@rocket/js 0.1.2 → 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 +8 -2
- 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 +60 -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/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.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 +70 -2
- package/exports/types.ts +7 -0
- package/package.json +15 -31
- 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/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 +3 -2
- package/src/layouts/atlas/atlasHeroLayout.js +4 -6
- package/src/layouts/atlas/atlasNotFoundLayout.js +9 -21
- 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
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/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
|
+
}
|
|
@@ -30,7 +30,7 @@ export function createComponentHydration({
|
|
|
30
30
|
serverOnlyComponents.push({ name, file, className, module });
|
|
31
31
|
} else if (loading === 'client') {
|
|
32
32
|
clientComponents.push({ name, file, className });
|
|
33
|
-
} else if (loading
|
|
33
|
+
} else if (isHydrateLoading(loading)) {
|
|
34
34
|
const module = await loadComponentModule(file);
|
|
35
35
|
hydratedComponents.push({
|
|
36
36
|
name,
|
|
@@ -39,13 +39,21 @@ export function createComponentHydration({
|
|
|
39
39
|
module,
|
|
40
40
|
strategy: loading.slice('hydrate:'.length),
|
|
41
41
|
});
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Registered Component "${name}" has an invalid loading value ${JSON.stringify(
|
|
45
|
+
loading,
|
|
46
|
+
)}. Use 'server', 'client', or 'hydrate:<strategy>'.`,
|
|
47
|
+
);
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
const registry = customElementsRegistry || customElements;
|
|
46
|
-
registry
|
|
52
|
+
// The @lit-labs/ssr-dom-shim registry keeps definitions between renders;
|
|
53
|
+
// spec-compliant registries have no such internals to reset.
|
|
54
|
+
registry.__definitions?.clear();
|
|
47
55
|
// @ts-ignore
|
|
48
|
-
registry.__reverseDefinitions
|
|
56
|
+
registry.__reverseDefinitions?.clear();
|
|
49
57
|
for (const component of serverOnlyComponents.concat(hydratedComponents)) {
|
|
50
58
|
if (!component.module[component.className]) {
|
|
51
59
|
throw new Error(
|
|
@@ -84,3 +92,15 @@ export function createComponentHydration({
|
|
|
84
92
|
return clientCode;
|
|
85
93
|
};
|
|
86
94
|
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {unknown} loading
|
|
98
|
+
* @returns {loading is string}
|
|
99
|
+
*/
|
|
100
|
+
function isHydrateLoading(loading) {
|
|
101
|
+
return (
|
|
102
|
+
typeof loading === 'string' &&
|
|
103
|
+
loading.startsWith('hydrate:') &&
|
|
104
|
+
loading.length > 'hydrate:'.length
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -13,13 +13,47 @@ export class RocketDrawer extends LitElement {
|
|
|
13
13
|
connectedCallback() {
|
|
14
14
|
super.connectedCallback();
|
|
15
15
|
this._onDocClick = this._onDocClick.bind(this);
|
|
16
|
+
this._onKeyDown = this._onKeyDown.bind(this);
|
|
16
17
|
document.addEventListener('click', this._onDocClick, { capture: true });
|
|
18
|
+
document.addEventListener('keydown', this._onKeyDown);
|
|
17
19
|
}
|
|
18
20
|
disconnectedCallback() {
|
|
19
21
|
document.removeEventListener('click', this._onDocClick, { capture: true });
|
|
22
|
+
document.removeEventListener('keydown', this._onKeyDown);
|
|
20
23
|
super.disconnectedCallback();
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @param {KeyboardEvent} ev
|
|
28
|
+
*/
|
|
29
|
+
_onKeyDown(ev) {
|
|
30
|
+
if (ev.key === 'Escape' && this.open) {
|
|
31
|
+
this.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {Map<string, unknown>} changedProperties
|
|
37
|
+
*/
|
|
38
|
+
updated(changedProperties) {
|
|
39
|
+
// skip the initial render so the drawer never steals focus on page load
|
|
40
|
+
if (!changedProperties.has('open') || changedProperties.get('open') === undefined) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (this.open) {
|
|
44
|
+
this.shadowRoot?.getElementById('content')?.focus();
|
|
45
|
+
} else {
|
|
46
|
+
const invokerSlot = /** @type {HTMLSlotElement | null} */ (
|
|
47
|
+
this.shadowRoot?.querySelector('slot[name="invoker"]') ?? null
|
|
48
|
+
);
|
|
49
|
+
const assigned = /** @type {HTMLElement | undefined} */ (invokerSlot?.assignedElements()[0]);
|
|
50
|
+
const fallback = /** @type {HTMLElement | null} */ (
|
|
51
|
+
this.shadowRoot?.querySelector('.burger-menu') ?? null
|
|
52
|
+
);
|
|
53
|
+
(assigned ?? fallback)?.focus();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
23
57
|
render() {
|
|
24
58
|
return html`
|
|
25
59
|
<slot name="invoker">
|
|
@@ -36,7 +70,7 @@ export class RocketDrawer extends LitElement {
|
|
|
36
70
|
</slot>
|
|
37
71
|
|
|
38
72
|
<dialog ?open=${this.open} id="rd-dialog">
|
|
39
|
-
<div id="content">
|
|
73
|
+
<div id="content" tabindex="-1">
|
|
40
74
|
<slot></slot>
|
|
41
75
|
</div>
|
|
42
76
|
</dialog>
|
|
@@ -149,23 +183,23 @@ export class RocketDrawer extends LitElement {
|
|
|
149
183
|
overflow: hidden;
|
|
150
184
|
opacity: 0;
|
|
151
185
|
pointer-events: none;
|
|
186
|
+
/* visibility keeps the closed drawer out of tab order and the a11y tree */
|
|
187
|
+
visibility: hidden;
|
|
152
188
|
transition:
|
|
153
189
|
width 0.35s var(--rd-ease-3),
|
|
154
|
-
opacity 0.35s var(--rd-ease-3)
|
|
190
|
+
opacity 0.35s var(--rd-ease-3),
|
|
191
|
+
visibility 0s var(--rd-ease-3) 0.35s;
|
|
155
192
|
will-change: width;
|
|
156
193
|
}
|
|
157
194
|
dialog[open] {
|
|
158
195
|
width: var(--rd-target-width);
|
|
159
196
|
opacity: 1;
|
|
160
197
|
pointer-events: auto;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
dialog[open]::backdrop {
|
|
168
|
-
opacity: 1;
|
|
198
|
+
visibility: visible;
|
|
199
|
+
transition:
|
|
200
|
+
width 0.35s var(--rd-ease-3),
|
|
201
|
+
opacity 0.35s var(--rd-ease-3),
|
|
202
|
+
visibility 0s;
|
|
169
203
|
}
|
|
170
204
|
|
|
171
205
|
#content {
|
|
@@ -2,14 +2,38 @@ import { readFileSync } from 'fs';
|
|
|
2
2
|
import { LitElement, html, css } from 'lit';
|
|
3
3
|
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
|
4
4
|
|
|
5
|
+
const SUPPORTED_BRANDS = new Set([
|
|
6
|
+
'discord',
|
|
7
|
+
'github',
|
|
8
|
+
'gitlab',
|
|
9
|
+
'license',
|
|
10
|
+
'npm',
|
|
11
|
+
'slack',
|
|
12
|
+
'telegram',
|
|
13
|
+
'twitter',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
/** @type {Map<string, string>} */
|
|
17
|
+
const iconCache = new Map();
|
|
18
|
+
|
|
5
19
|
/**
|
|
6
20
|
* @param {string} brandName
|
|
7
21
|
* @returns {import('lit/directive.js').DirectiveResult}
|
|
8
22
|
*/
|
|
9
23
|
function getIcon(brandName) {
|
|
10
24
|
const brand = brandName.toLowerCase();
|
|
11
|
-
|
|
12
|
-
|
|
25
|
+
if (!SUPPORTED_BRANDS.has(brand)) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`rocket-social-link does not support name="${brandName}". ` +
|
|
28
|
+
`Supported names: ${[...SUPPORTED_BRANDS].join(', ')}.`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
let fileContent = iconCache.get(brand);
|
|
32
|
+
if (fileContent === undefined) {
|
|
33
|
+
fileContent = readFileSync(new URL(`./assets/${brand}.svg`, import.meta.url), 'utf8');
|
|
34
|
+
iconCache.set(brand, fileContent);
|
|
35
|
+
}
|
|
36
|
+
return unsafeHTML(fileContent);
|
|
13
37
|
}
|
|
14
38
|
|
|
15
39
|
export class RocketSocialLink extends LitElement {
|
package/src/config.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Runs on: server */
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
3
5
|
import { validateIconLibrariesConfig } from './icons.js';
|
|
4
6
|
|
|
5
7
|
const REDIRECT_STATUSES = new Set([301, 302, 307, 308]);
|
|
@@ -9,14 +11,25 @@ const REDIRECT_STATUSES = new Set([301, 302, 307, 308]);
|
|
|
9
11
|
* @returns {Promise<import("@rocket/js/types.js").ResolvedRocketConfig>}
|
|
10
12
|
*/
|
|
11
13
|
export async function readConfig(filePath) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
const configPath = path.join(process.cwd(), filePath || 'rocket-config.js');
|
|
15
|
+
if (!existsSync(configPath)) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`No Rocket config found at ${configPath}. ` +
|
|
18
|
+
`Run "npx rocket init" to create one, or pass --config-file <path>.`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
const configFile = await import(pathToFileURL(configPath).href);
|
|
15
22
|
if (configFile.default === undefined) {
|
|
16
23
|
throw new Error('rocket-config.js must have a default export');
|
|
17
24
|
}
|
|
18
25
|
/** @type {import("@rocket/js/types.js").RocketConfig} */
|
|
19
26
|
const config = configFile.default;
|
|
27
|
+
if (!Array.isArray(config.includeGlobs)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Invalid Rocket config in ${configPath}: includeGlobs must be an array of glob strings, ` +
|
|
30
|
+
`e.g. includeGlobs: ['docs/pages/**/*.rocket.{md,js}'].`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
20
33
|
validateRedirectConfig(config.urlLifecycle?.redirects);
|
|
21
34
|
const siteHeadMetadata = normalizeSiteHeadMetadataConfig(config.siteHeadMetadata);
|
|
22
35
|
validateIconLibrariesConfig(config.iconLibraries);
|
|
@@ -24,7 +37,7 @@ export async function readConfig(filePath) {
|
|
|
24
37
|
const siteOrigin = siteHeadMetadata
|
|
25
38
|
? requireSiteHeadMetadataSiteOrigin(config.siteOrigin)
|
|
26
39
|
: config.siteOrigin;
|
|
27
|
-
|
|
40
|
+
const includeGlobs = config.includeGlobs.map(glob => {
|
|
28
41
|
if (!glob.includes('*') && !glob.includes('.')) {
|
|
29
42
|
// If the glob is a directory, include all files in the directory
|
|
30
43
|
return `${glob}${glob.endsWith('/') ? '' : '/'}**`;
|
|
@@ -36,6 +49,7 @@ export async function readConfig(filePath) {
|
|
|
36
49
|
excludeRegex: [],
|
|
37
50
|
adjustDevServerConfig: _ => _,
|
|
38
51
|
...config,
|
|
52
|
+
includeGlobs,
|
|
39
53
|
...(siteOrigin ? { siteOrigin } : {}),
|
|
40
54
|
...(siteHeadMetadata ? { siteHeadMetadata } : {}),
|
|
41
55
|
};
|
|
@@ -248,6 +262,14 @@ function invalidSiteOriginError(siteOrigin) {
|
|
|
248
262
|
function validateRedirectConfig(redirects = []) {
|
|
249
263
|
const sources = new Set();
|
|
250
264
|
for (const redirect of redirects) {
|
|
265
|
+
// whitespace would corrupt whitespace-delimited outputs like Netlify _redirects
|
|
266
|
+
if (containsWhitespace(redirect.source) || containsWhitespace(redirect.target)) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`Invalid Redirect from ${JSON.stringify(redirect.source)} to ${JSON.stringify(
|
|
269
|
+
redirect.target,
|
|
270
|
+
)}. Redirect paths must not contain whitespace; percent-encode it instead (%20).`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
251
273
|
if (!isInternalAbsolutePath(redirect.source)) {
|
|
252
274
|
throw new Error(
|
|
253
275
|
`Invalid Redirect source ${JSON.stringify(
|
|
@@ -283,6 +305,13 @@ function isInternalAbsolutePath(value) {
|
|
|
283
305
|
return typeof value === 'string' && value.startsWith('/') && !value.startsWith('//');
|
|
284
306
|
}
|
|
285
307
|
|
|
308
|
+
/**
|
|
309
|
+
* @param {unknown} value
|
|
310
|
+
*/
|
|
311
|
+
function containsWhitespace(value) {
|
|
312
|
+
return typeof value === 'string' && /\s/.test(value);
|
|
313
|
+
}
|
|
314
|
+
|
|
286
315
|
/**
|
|
287
316
|
* @param {unknown} value
|
|
288
317
|
*/
|