@rocket/js 0.0.0 → 0.1.1
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/LICENSE +21 -0
- package/README.md +233 -2
- package/dist-types/exports/MainMenu.d.ts +2 -0
- package/dist-types/exports/MainMenu.d.ts.map +1 -0
- package/dist-types/exports/PageData.d.ts +2 -0
- package/dist-types/exports/PageData.d.ts.map +1 -0
- package/dist-types/exports/RocketCodeBlock.d.ts +2 -0
- package/dist-types/exports/RocketCodeBlock.d.ts.map +1 -0
- package/dist-types/exports/RocketIcon.d.ts +2 -0
- package/dist-types/exports/RocketIcon.d.ts.map +1 -0
- package/dist-types/exports/RocketJsDemo.d.ts +2 -0
- package/dist-types/exports/RocketJsDemo.d.ts.map +1 -0
- package/dist-types/exports/RocketRequestDemo.d.ts +2 -0
- package/dist-types/exports/RocketRequestDemo.d.ts.map +1 -0
- package/dist-types/exports/SocialPreviewPlayground.d.ts +2 -0
- package/dist-types/exports/SocialPreviewPlayground.d.ts.map +1 -0
- package/dist-types/exports/adapters/netlify.d.ts +2 -0
- package/dist-types/exports/adapters/netlify.d.ts.map +1 -0
- package/dist-types/exports/asyncMessage.d.ts +2 -0
- package/dist-types/exports/asyncMessage.d.ts.map +1 -0
- package/dist-types/exports/component-hydration.d.ts +2 -0
- package/dist-types/exports/component-hydration.d.ts.map +1 -0
- package/dist-types/exports/components/web-awesome.d.ts +3 -0
- package/dist-types/exports/components/web-awesome.d.ts.map +1 -0
- package/dist-types/exports/components.d.ts +2 -0
- package/dist-types/exports/components.d.ts.map +1 -0
- package/dist-types/exports/config.d.ts +2 -0
- package/dist-types/exports/config.d.ts.map +1 -0
- package/dist-types/exports/debounce.d.ts +2 -0
- package/dist-types/exports/debounce.d.ts.map +1 -0
- package/dist-types/exports/define/RocketCodeBlock.d.ts +2 -0
- package/dist-types/exports/define/RocketCodeBlock.d.ts.map +1 -0
- package/dist-types/exports/define/RocketIcon.d.ts +2 -0
- package/dist-types/exports/define/RocketIcon.d.ts.map +1 -0
- package/dist-types/exports/define/RocketJsDemo.d.ts +2 -0
- package/dist-types/exports/define/RocketJsDemo.d.ts.map +1 -0
- package/dist-types/exports/define/RocketRequestDemo.d.ts +2 -0
- package/dist-types/exports/define/RocketRequestDemo.d.ts.map +1 -0
- package/dist-types/exports/define/menus.d.ts +2 -0
- package/dist-types/exports/define/menus.d.ts.map +1 -0
- package/dist-types/exports/extractCode.d.ts +2 -0
- package/dist-types/exports/extractCode.d.ts.map +1 -0
- package/dist-types/exports/globalData.d.ts +2 -0
- package/dist-types/exports/globalData.d.ts.map +1 -0
- package/dist-types/exports/hydration/hydrationLoader.d.ts +2 -0
- package/dist-types/exports/hydration/hydrationLoader.d.ts.map +1 -0
- package/dist-types/exports/icons.d.ts +2 -0
- package/dist-types/exports/icons.d.ts.map +1 -0
- package/dist-types/exports/layout-helper.d.ts +2 -0
- package/dist-types/exports/layout-helper.d.ts.map +1 -0
- package/dist-types/exports/layout.d.ts +2 -0
- package/dist-types/exports/layout.d.ts.map +1 -0
- package/dist-types/exports/layouts/atlasDoc.d.ts +2 -0
- package/dist-types/exports/layouts/atlasDoc.d.ts.map +1 -0
- package/dist-types/exports/layouts/atlasHero.d.ts +2 -0
- package/dist-types/exports/layouts/atlasHero.d.ts.map +1 -0
- package/dist-types/exports/layouts/atlasNotFound.d.ts +2 -0
- package/dist-types/exports/layouts/atlasNotFound.d.ts.map +1 -0
- package/dist-types/exports/loaded-page-module.d.ts +2 -0
- package/dist-types/exports/loaded-page-module.d.ts.map +1 -0
- package/dist-types/exports/markdownHook.d.ts +2 -0
- package/dist-types/exports/markdownHook.d.ts.map +1 -0
- package/dist-types/exports/menu.d.ts +2 -0
- package/dist-types/exports/menu.d.ts.map +1 -0
- package/dist-types/exports/menus.d.ts +6 -0
- package/dist-types/exports/menus.d.ts.map +1 -0
- package/dist-types/exports/page-runtime.d.ts +2 -0
- package/dist-types/exports/page-runtime.d.ts.map +1 -0
- package/dist-types/exports/pages.d.ts +2 -0
- package/dist-types/exports/pages.d.ts.map +1 -0
- package/dist-types/exports/resolve.d.ts +2 -0
- package/dist-types/exports/resolve.d.ts.map +1 -0
- package/dist-types/exports/ssr.d.ts +2 -0
- package/dist-types/exports/ssr.d.ts.map +1 -0
- package/dist-types/exports/standalone-demo-url.d.ts +2 -0
- package/dist-types/exports/standalone-demo-url.d.ts.map +1 -0
- package/dist-types/exports/transform.d.ts +2 -0
- package/dist-types/exports/transform.d.ts.map +1 -0
- package/dist-types/exports/types/hydration.d.ts +23 -0
- package/dist-types/exports/types/hydration.d.ts.map +1 -0
- package/dist-types/exports/types/rocket.d.ts +504 -0
- package/dist-types/exports/types/rocket.d.ts.map +1 -0
- package/dist-types/exports/types.d.ts +3 -0
- package/dist-types/exports/types.d.ts.map +1 -0
- package/dist-types/exports/wds-plugin.d.ts +2 -0
- package/dist-types/exports/wds-plugin.d.ts.map +1 -0
- package/dist-types/src/PageData.d.ts +82 -0
- package/dist-types/src/PageData.d.ts.map +1 -0
- package/dist-types/src/RocketCodeBlock.d.ts +64 -0
- package/dist-types/src/RocketCodeBlock.d.ts.map +1 -0
- package/dist-types/src/RocketIcon.d.ts +35 -0
- package/dist-types/src/RocketIcon.d.ts.map +1 -0
- package/dist-types/src/RocketJsDemo.d.ts +59 -0
- package/dist-types/src/RocketJsDemo.d.ts.map +1 -0
- package/dist-types/src/RocketJsDemo.test-browser.d.ts +3 -0
- package/dist-types/src/RocketJsDemo.test-browser.d.ts.map +1 -0
- package/dist-types/src/RocketRequestDemo.d.ts +57 -0
- package/dist-types/src/RocketRequestDemo.d.ts.map +1 -0
- package/dist-types/src/RocketRequestDemo.test-browser.d.ts +3 -0
- package/dist-types/src/RocketRequestDemo.test-browser.d.ts.map +1 -0
- package/dist-types/src/SocialPreviewPlayground.d.ts +102 -0
- package/dist-types/src/SocialPreviewPlayground.d.ts.map +1 -0
- package/dist-types/src/adapters/netlify.d.ts +54 -0
- package/dist-types/src/adapters/netlify.d.ts.map +1 -0
- package/dist-types/src/asyncMessage.d.ts +14 -0
- package/dist-types/src/asyncMessage.d.ts.map +1 -0
- package/dist-types/src/cli/RocketBuild.d.ts +78 -0
- package/dist-types/src/cli/RocketBuild.d.ts.map +1 -0
- package/dist-types/src/cli/RocketCli.d.ts +17 -0
- package/dist-types/src/cli/RocketCli.d.ts.map +1 -0
- package/dist-types/src/cli/RocketInit.d.ts +22 -0
- package/dist-types/src/cli/RocketInit.d.ts.map +1 -0
- package/dist-types/src/cli/RocketStart.d.ts +45 -0
- package/dist-types/src/cli/RocketStart.d.ts.map +1 -0
- package/dist-types/src/cli/cli.d.ts +3 -0
- package/dist-types/src/cli/cli.d.ts.map +1 -0
- package/dist-types/src/component-hydration.d.ts +26 -0
- package/dist-types/src/component-hydration.d.ts.map +1 -0
- package/dist-types/src/components/FeatureList.d.ts +15 -0
- package/dist-types/src/components/FeatureList.d.ts.map +1 -0
- package/dist-types/src/components/Footer.d.ts +17 -0
- package/dist-types/src/components/Footer.d.ts.map +1 -0
- package/dist-types/src/components/Header.d.ts +6 -0
- package/dist-types/src/components/Header.d.ts.map +1 -0
- package/dist-types/src/components/RocketDrawer.d.ts +20 -0
- package/dist-types/src/components/RocketDrawer.d.ts.map +1 -0
- package/dist-types/src/components/RocketSocialLink.d.ts +30 -0
- package/dist-types/src/components/RocketSocialLink.d.ts.map +1 -0
- package/dist-types/src/components.d.ts +7 -0
- package/dist-types/src/components.d.ts.map +1 -0
- package/dist-types/src/config.d.ts +6 -0
- package/dist-types/src/config.d.ts.map +1 -0
- package/dist-types/src/debounce.d.ts +8 -0
- package/dist-types/src/debounce.d.ts.map +1 -0
- package/dist-types/src/defaultSocialPreviewTemplate.d.ts +31 -0
- package/dist-types/src/defaultSocialPreviewTemplate.d.ts.map +1 -0
- package/dist-types/src/development-page-module-loader.d.ts +15 -0
- package/dist-types/src/development-page-module-loader.d.ts.map +1 -0
- package/dist-types/src/extractCode.d.ts +5 -0
- package/dist-types/src/extractCode.d.ts.map +1 -0
- package/dist-types/src/hydration/evaluate.d.ts +20 -0
- package/dist-types/src/hydration/evaluate.d.ts.map +1 -0
- package/dist-types/src/hydration/extractStrategies.d.ts +5 -0
- package/dist-types/src/hydration/extractStrategies.d.ts.map +1 -0
- package/dist-types/src/hydration/hydrationLoader.d.ts +64 -0
- package/dist-types/src/hydration/hydrationLoader.d.ts.map +1 -0
- package/dist-types/src/icons.d.ts +182 -0
- package/dist-types/src/icons.d.ts.map +1 -0
- package/dist-types/src/layouts/atlas/atlasDocLayout.d.ts +45 -0
- package/dist-types/src/layouts/atlas/atlasDocLayout.d.ts.map +1 -0
- package/dist-types/src/layouts/atlas/atlasHeroLayout.d.ts +7 -0
- package/dist-types/src/layouts/atlas/atlasHeroLayout.d.ts.map +1 -0
- package/dist-types/src/layouts/atlas/atlasNotFoundLayout.d.ts +5 -0
- package/dist-types/src/layouts/atlas/atlasNotFoundLayout.d.ts.map +1 -0
- package/dist-types/src/layouts/layout-helper.d.ts +16 -0
- package/dist-types/src/layouts/layout-helper.d.ts.map +1 -0
- package/dist-types/src/layouts/layout.d.ts +7 -0
- package/dist-types/src/layouts/layout.d.ts.map +1 -0
- package/dist-types/src/loaded-page-module.d.ts +51 -0
- package/dist-types/src/loaded-page-module.d.ts.map +1 -0
- package/dist-types/src/main.d.ts +2 -0
- package/dist-types/src/main.d.ts.map +1 -0
- package/dist-types/src/markdownCompiler.d.ts +22 -0
- package/dist-types/src/markdownCompiler.d.ts.map +1 -0
- package/dist-types/src/markdownHook.d.ts +6 -0
- package/dist-types/src/markdownHook.d.ts.map +1 -0
- package/dist-types/src/menu.d.ts +22 -0
- package/dist-types/src/menu.d.ts.map +1 -0
- package/dist-types/src/menus/MainMenu.d.ts +23 -0
- package/dist-types/src/menus/MainMenu.d.ts.map +1 -0
- package/dist-types/src/menus/RocketMenu.d.ts +23 -0
- package/dist-types/src/menus/RocketMenu.d.ts.map +1 -0
- package/dist-types/src/menus/RocketNextPage.d.ts +18 -0
- package/dist-types/src/menus/RocketNextPage.d.ts.map +1 -0
- package/dist-types/src/menus/RocketPreviousPage.d.ts +18 -0
- package/dist-types/src/menus/RocketPreviousPage.d.ts.map +1 -0
- package/dist-types/src/menus/RocketToc.d.ts +54 -0
- package/dist-types/src/menus/RocketToc.d.ts.map +1 -0
- package/dist-types/src/menus/pageNavigation.d.ts +41 -0
- package/dist-types/src/menus/pageNavigation.d.ts.map +1 -0
- package/dist-types/src/page-pagination.d.ts +69 -0
- package/dist-types/src/page-pagination.d.ts.map +1 -0
- package/dist-types/src/page-runtime.d.ts +110 -0
- package/dist-types/src/page-runtime.d.ts.map +1 -0
- package/dist-types/src/pages.d.ts +10 -0
- package/dist-types/src/pages.d.ts.map +1 -0
- package/dist-types/src/publicAssets.d.ts +70 -0
- package/dist-types/src/publicAssets.d.ts.map +1 -0
- package/dist-types/src/requestDemoMetadata.d.ts +19 -0
- package/dist-types/src/requestDemoMetadata.d.ts.map +1 -0
- package/dist-types/src/resolve.d.ts +7 -0
- package/dist-types/src/resolve.d.ts.map +1 -0
- package/dist-types/src/siteDiscoverability.d.ts +33 -0
- package/dist-types/src/siteDiscoverability.d.ts.map +1 -0
- package/dist-types/src/siteHeadMetadata.d.ts +20 -0
- package/dist-types/src/siteHeadMetadata.d.ts.map +1 -0
- package/dist-types/src/socialPreviewImages.d.ts +186 -0
- package/dist-types/src/socialPreviewImages.d.ts.map +1 -0
- package/dist-types/src/socialPreviewTemplatePreview.d.ts +22 -0
- package/dist-types/src/socialPreviewTemplatePreview.d.ts.map +1 -0
- package/dist-types/src/ssr.d.ts +6 -0
- package/dist-types/src/ssr.d.ts.map +1 -0
- package/dist-types/src/standalone-demo-url.d.ts +60 -0
- package/dist-types/src/standalone-demo-url.d.ts.map +1 -0
- package/dist-types/src/static-page-module-loader.d.ts +15 -0
- package/dist-types/src/static-page-module-loader.d.ts.map +1 -0
- package/dist-types/src/transform.d.ts +10 -0
- package/dist-types/src/transform.d.ts.map +1 -0
- package/dist-types/src/urlLifecycle.d.ts +23 -0
- package/dist-types/src/urlLifecycle.d.ts.map +1 -0
- package/dist-types/src/wds-plugin.d.ts +15 -0
- package/dist-types/src/wds-plugin.d.ts.map +1 -0
- package/docs/assets/home-background.svg +1 -0
- package/docs/assets/prism-one-light.css +368 -0
- package/docs/assets/rocket-logo-dark-with-text-below.svg +8 -0
- package/docs/assets/rocket-logo-dark-with-text.svg +7 -0
- package/docs/assets/rocket-logo-dark.svg +7 -0
- package/docs/assets/rocket-logo-light-with-text-below.svg +14 -0
- package/docs/assets/rocket-logo-light-with-text.svg +13 -0
- package/docs/assets/rocket-logo-light.svg +12 -0
- package/docs/assets/rocket-text-no-logo.svg +3 -0
- package/exports/MainMenu.js +1 -0
- package/exports/PageData.js +1 -0
- package/exports/RocketCodeBlock.js +1 -0
- package/exports/RocketIcon.js +1 -0
- package/exports/RocketJsDemo.js +1 -0
- package/exports/RocketRequestDemo.js +1 -0
- package/exports/SocialPreviewPlayground.js +1 -0
- package/exports/adapters/netlify.js +1 -0
- package/exports/asyncMessage.js +1 -0
- package/exports/component-hydration.js +1 -0
- package/exports/components/web-awesome.js +63 -0
- package/exports/components.js +1 -0
- package/exports/config.js +1 -0
- package/exports/debounce.js +1 -0
- package/exports/define/RocketCodeBlock.js +2 -0
- package/exports/define/RocketIcon.js +3 -0
- package/exports/define/RocketJsDemo.js +5 -0
- package/exports/define/RocketRequestDemo.js +5 -0
- package/exports/define/menus.js +14 -0
- package/exports/extractCode.js +1 -0
- package/exports/globalData.js +1 -0
- package/exports/hydration/hydrationLoader.js +1 -0
- package/exports/icons.js +11 -0
- package/exports/layout-helper.js +1 -0
- package/exports/layout.js +1 -0
- package/exports/layouts/_atlas.css +3 -0
- package/exports/layouts/atlasDoc.js +5 -0
- package/exports/layouts/atlasHero.js +1 -0
- package/exports/layouts/atlasNotFound.js +4 -0
- package/exports/loaded-page-module.js +5 -0
- package/exports/markdownHook.js +4 -0
- package/exports/menu.js +4 -0
- package/exports/menus.js +5 -0
- package/exports/page-runtime.js +5 -0
- package/exports/pages.js +1 -0
- package/exports/resolve.js +1 -0
- package/exports/ssr.js +1 -0
- package/exports/standalone-demo-url.js +10 -0
- package/exports/transform.js +1 -0
- package/exports/types/hydration.ts +26 -0
- package/exports/types/rocket.ts +598 -0
- package/exports/types.ts +71 -0
- package/exports/wds-plugin.js +1 -0
- package/package.json +192 -9
- package/src/PageData.js +244 -0
- package/src/RocketCodeBlock.js +516 -0
- package/src/RocketIcon.js +291 -0
- package/src/RocketJsDemo.js +397 -0
- package/src/RocketJsDemo.test-browser.js +228 -0
- package/src/RocketRequestDemo.js +439 -0
- package/src/RocketRequestDemo.test-browser.js +301 -0
- package/src/SocialPreviewPlayground.js +573 -0
- package/src/adapters/netlify.js +814 -0
- package/src/asyncMessage.js +21 -0
- package/src/cli/RocketBuild.js +581 -0
- package/src/cli/RocketCli.js +47 -0
- package/src/cli/RocketInit.js +636 -0
- package/src/cli/RocketStart.js +145 -0
- package/src/cli/cli.js +7 -0
- package/src/component-hydration.js +86 -0
- package/src/components/FeatureList.js +114 -0
- package/src/components/Footer.js +116 -0
- package/src/components/Header.js +122 -0
- package/src/components/RocketDrawer.js +193 -0
- package/src/components/RocketSocialLink.js +128 -0
- package/src/components/assets/discord.svg +7 -0
- package/src/components/assets/github.svg +4 -0
- package/src/components/assets/gitlab.svg +1 -0
- package/src/components/assets/info.txt +1 -0
- package/src/components/assets/license.svg +3 -0
- package/src/components/assets/npm.svg +5 -0
- package/src/components/assets/slack.svg +5 -0
- package/src/components/assets/telegram.svg +4 -0
- package/src/components/assets/twitter.svg +1 -0
- package/src/components.js +34 -0
- package/src/config.js +319 -0
- package/src/debounce.js +21 -0
- package/src/defaultSocialPreviewTemplate.js +118 -0
- package/src/development-page-module-loader.js +29 -0
- package/src/extractCode.js +41 -0
- package/src/hydration/evaluate.js +54 -0
- package/src/hydration/extractStrategies.js +91 -0
- package/src/hydration/hydrationLoader.js +330 -0
- package/src/icons.js +898 -0
- package/src/layouts/atlas/atlasDoc.css +877 -0
- package/src/layouts/atlas/atlasDocLayout.js +275 -0
- package/src/layouts/atlas/atlasHero.css +774 -0
- package/src/layouts/atlas/atlasHeroLayout.js +337 -0
- package/src/layouts/atlas/atlasNotFound.css +365 -0
- package/src/layouts/atlas/atlasNotFoundLayout.js +69 -0
- package/src/layouts/layout-helper.js +92 -0
- package/src/layouts/layout.js +52 -0
- package/src/loaded-page-module.js +97 -0
- package/src/main.js +72 -0
- package/src/markdownCompiler.js +303 -0
- package/src/markdownHook.js +148 -0
- package/src/menu.js +210 -0
- package/src/menus/MainMenu.js +58 -0
- package/src/menus/RocketMenu.js +191 -0
- package/src/menus/RocketNextPage.js +25 -0
- package/src/menus/RocketPreviousPage.js +29 -0
- package/src/menus/RocketToc.js +309 -0
- package/src/menus/pageNavigation.js +285 -0
- package/src/page-pagination.js +241 -0
- package/src/page-runtime.js +481 -0
- package/src/pages.js +537 -0
- package/src/publicAssets.js +336 -0
- package/src/requestDemoMetadata.js +97 -0
- package/src/resolve.js +15 -0
- package/src/siteDiscoverability.js +184 -0
- package/src/siteHeadMetadata.js +69 -0
- package/src/socialPreviewImages.js +482 -0
- package/src/socialPreviewTemplatePreview.js +352 -0
- package/src/ssr.js +14 -0
- package/src/standalone-demo-url.js +147 -0
- package/src/static-page-module-loader.js +29 -0
- package/src/transform.js +720 -0
- package/src/urlLifecycle.js +57 -0
- package/src/wds-plugin.js +307 -0
package/src/icons.js
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
/** Runs on: server */
|
|
2
|
+
import { glob, readFile } from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import * as parse5 from 'parse5';
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
|
|
11
|
+
const ICON_LOADING_VALUES = new Set(['auto', 'server', 'client']);
|
|
12
|
+
const ICON_REFERENCE_CONFIG_FIELDS = new Set(['library', 'name']);
|
|
13
|
+
const ROCKET_ICON_ASSET_PREFIX = '/_rocket/icons/';
|
|
14
|
+
const ROCKET_ICON_DEFINE_MODULE_PATH = '/_rocket/rocket-icon.js';
|
|
15
|
+
const ROCKET_ICON_CLASS_MODULE_PATH = '/_rocket/RocketIcon.js';
|
|
16
|
+
const SHADOW_ICON_STYLE =
|
|
17
|
+
':host{display:inline-block;width:1em;height:1em;vertical-align:-0.125em;line-height:1}' +
|
|
18
|
+
':host([hidden]){display:none}' +
|
|
19
|
+
'span[part="icon"]{display:inline-flex;width:100%;height:100%;line-height:1}' +
|
|
20
|
+
'span[part="icon"]>svg{display:block;width:100%;height:100%}';
|
|
21
|
+
|
|
22
|
+
/** @type {Map<string, Promise<Map<string, string>>>} */
|
|
23
|
+
const libraryIndexCache = new Map();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} packageName
|
|
27
|
+
* @param {string} files
|
|
28
|
+
* @returns {import('@rocket/js/types.js').IconLibrarySource}
|
|
29
|
+
*/
|
|
30
|
+
export function iconsFromPackage(packageName, files) {
|
|
31
|
+
return {
|
|
32
|
+
type: 'package',
|
|
33
|
+
packageName: readNonEmptyString(packageName, 'Icon package name'),
|
|
34
|
+
files: readNonEmptyString(files, 'Icon package source files'),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} files
|
|
40
|
+
* @returns {import('@rocket/js/types.js').IconLibrarySource}
|
|
41
|
+
*/
|
|
42
|
+
export function iconsFromPath(files) {
|
|
43
|
+
return {
|
|
44
|
+
type: 'path',
|
|
45
|
+
files: readNonEmptyString(files, 'Icon path source files'),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const rocketBootstrapIconLibraries = {
|
|
50
|
+
bootstrap: iconsFromPackage('bootstrap-icons', 'icons/*.svg'),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const rocketDefaultBootstrapIconLibrary = 'bootstrap';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {{ addIconLibraries: (iconLibraries: import('@rocket/js/types.js').IconLibrariesConfig, options?: { defaultIconLibrary?: string }) => void }} pageData
|
|
57
|
+
*/
|
|
58
|
+
export function addBootstrapIconLibrary(pageData) {
|
|
59
|
+
pageData.addIconLibraries(rocketBootstrapIconLibraries, {
|
|
60
|
+
defaultIconLibrary: rocketDefaultBootstrapIconLibrary,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class IconAssetStore {
|
|
65
|
+
constructor() {
|
|
66
|
+
/** @type {Map<string, { url: string; svg: string; library: string; name: string }>} */
|
|
67
|
+
this.assets = new Map();
|
|
68
|
+
this.needsRuntime = false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {{ library: string; name: string; svg: string }} icon
|
|
73
|
+
*/
|
|
74
|
+
addIcon({ library, name, svg }) {
|
|
75
|
+
const url = iconAssetUrl({ library, name, svg });
|
|
76
|
+
if (!this.assets.has(url)) {
|
|
77
|
+
this.assets.set(url, { url, svg, library, name });
|
|
78
|
+
}
|
|
79
|
+
return url;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} url
|
|
84
|
+
*/
|
|
85
|
+
get(url) {
|
|
86
|
+
return this.assets.get(url);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
outputs() {
|
|
90
|
+
return [...this.assets.values()];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createIconAssetStore() {
|
|
95
|
+
return new IconAssetStore();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function rocketIconRuntimeOutputs() {
|
|
99
|
+
return [
|
|
100
|
+
{
|
|
101
|
+
path: ROCKET_ICON_DEFINE_MODULE_PATH,
|
|
102
|
+
type: 'application/javascript',
|
|
103
|
+
data:
|
|
104
|
+
`import { RocketIcon } from '${ROCKET_ICON_CLASS_MODULE_PATH}';\n` +
|
|
105
|
+
"if (!customElements.get('rocket-icon')) {\n" +
|
|
106
|
+
" customElements.define('rocket-icon', RocketIcon);\n" +
|
|
107
|
+
'}\n',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
path: ROCKET_ICON_CLASS_MODULE_PATH,
|
|
111
|
+
type: 'application/javascript',
|
|
112
|
+
data: readFileSync(new URL('./RocketIcon.js', import.meta.url), 'utf8'),
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {string} requestPath
|
|
119
|
+
* @param {{
|
|
120
|
+
* iconLibraries?: import('@rocket/js/types.js').IconLibrariesConfig;
|
|
121
|
+
* defaultIconLibrary?: string;
|
|
122
|
+
* layoutIconLibraries?: import('@rocket/js/types.js').IconLibrariesConfig | Map<string, import('@rocket/js/types.js').NormalizedIconLibraryConfig>;
|
|
123
|
+
* layoutDefaultIconLibrary?: string;
|
|
124
|
+
* }} [options]
|
|
125
|
+
* @returns {Promise<{ url: string; svg: string; library: string; name: string } | undefined>}
|
|
126
|
+
*/
|
|
127
|
+
export async function resolveRocketIconAsset(requestPath, options = {}) {
|
|
128
|
+
const match = /^\/_rocket\/icons\/([^/]+)\/(.+)\.([a-f0-9]{12})\.svg$/.exec(requestPath);
|
|
129
|
+
if (!match) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const librarySegment = match[1];
|
|
134
|
+
const resolver = createIconResolver({
|
|
135
|
+
layoutIconLibraries: options.layoutIconLibraries,
|
|
136
|
+
layoutDefaultIconLibrary: options.layoutDefaultIconLibrary,
|
|
137
|
+
projectIconLibraries: options.iconLibraries,
|
|
138
|
+
projectDefaultIconLibrary: options.defaultIconLibrary,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
for (const [library, config] of resolver.libraries) {
|
|
142
|
+
if (sanitizePathSegment(library) !== librarySegment) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const icons = await indexIconLibrary(library, config);
|
|
146
|
+
for (const [name, svg] of icons) {
|
|
147
|
+
const url = iconAssetUrl({ library, name, svg });
|
|
148
|
+
if (url === requestPath) {
|
|
149
|
+
return { url, svg, library, name };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {unknown} iconLibraries
|
|
159
|
+
*/
|
|
160
|
+
export function validateIconLibrariesConfig(iconLibraries) {
|
|
161
|
+
normalizeIconLibrariesConfig(iconLibraries, 'Icon Library Configuration');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} html
|
|
166
|
+
* @param {{
|
|
167
|
+
* iconLibraries?: import('@rocket/js/types.js').IconLibrariesConfig;
|
|
168
|
+
* defaultIconLibrary?: string;
|
|
169
|
+
* layoutIconLibraries?: import('@rocket/js/types.js').IconLibrariesConfig | Map<string, import('@rocket/js/types.js').NormalizedIconLibraryConfig>;
|
|
170
|
+
* layoutDefaultIconLibrary?: string;
|
|
171
|
+
* pageData?: {
|
|
172
|
+
* _iconLibraries?: Map<string, import('@rocket/js/types.js').NormalizedIconLibraryConfig>;
|
|
173
|
+
* _defaultIconLibrary?: string;
|
|
174
|
+
* _iconReferences?: import('@rocket/js/types.js').IconReferenceConfig[];
|
|
175
|
+
* _hydrationScript?: string;
|
|
176
|
+
* _hasBrowserLoadedComponents?: boolean;
|
|
177
|
+
* };
|
|
178
|
+
* iconAssetStore?: IconAssetStore;
|
|
179
|
+
* }} [options]
|
|
180
|
+
* @returns {Promise<string>}
|
|
181
|
+
*/
|
|
182
|
+
export async function finalizeRocketIcons(html, options = {}) {
|
|
183
|
+
const lowerHtml = html.toLowerCase();
|
|
184
|
+
const hasIconHosts = lowerHtml.includes('<rocket-icon');
|
|
185
|
+
const hasIconLoadingPolicy =
|
|
186
|
+
lowerHtml.includes('icon-loading-region') || lowerHtml.includes('icon-server-budget');
|
|
187
|
+
const extraIconReferences = options.pageData?._iconReferences || [];
|
|
188
|
+
const hasExtraIconReferences = extraIconReferences.length > 0;
|
|
189
|
+
if (
|
|
190
|
+
!hasIconHosts &&
|
|
191
|
+
!hasIconLoadingPolicy &&
|
|
192
|
+
!pageNeedsDeferredIconRuntime(options.pageData) &&
|
|
193
|
+
!hasExtraIconReferences
|
|
194
|
+
) {
|
|
195
|
+
return html;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const resolver = createIconResolver({
|
|
199
|
+
layoutIconLibraries: options.layoutIconLibraries || options.pageData?._iconLibraries,
|
|
200
|
+
layoutDefaultIconLibrary:
|
|
201
|
+
options.layoutDefaultIconLibrary || options.pageData?._defaultIconLibrary,
|
|
202
|
+
projectIconLibraries: options.iconLibraries,
|
|
203
|
+
projectDefaultIconLibrary: options.defaultIconLibrary,
|
|
204
|
+
});
|
|
205
|
+
const iconAssetStore = options.iconAssetStore || createIconAssetStore();
|
|
206
|
+
/** @type {Map<string, { library: string; name: string; svg?: string }>} */
|
|
207
|
+
const iconReferences = new Map();
|
|
208
|
+
let hasClientIcon = false;
|
|
209
|
+
|
|
210
|
+
const replacements = await finalizeRocketIconHosts(html, resolver);
|
|
211
|
+
for (const replacement of replacements) {
|
|
212
|
+
const renderedIcon = replacement.renderedIcon;
|
|
213
|
+
iconReferences.set(iconReferenceKey(renderedIcon.library, renderedIcon.name), {
|
|
214
|
+
library: renderedIcon.library,
|
|
215
|
+
name: renderedIcon.name,
|
|
216
|
+
svg: renderedIcon.svg || undefined,
|
|
217
|
+
});
|
|
218
|
+
if (renderedIcon.iconLoading === 'client') {
|
|
219
|
+
hasClientIcon = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const iconReference of extraIconReferences) {
|
|
223
|
+
const library = resolver.resolveLibrary(iconReference.library, iconReference.name);
|
|
224
|
+
const key = iconReferenceKey(library, iconReference.name);
|
|
225
|
+
if (!iconReferences.has(key)) {
|
|
226
|
+
iconReferences.set(key, {
|
|
227
|
+
library,
|
|
228
|
+
name: iconReference.name,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const output = applyHtmlReplacements(html, replacements);
|
|
234
|
+
|
|
235
|
+
const needsManifest =
|
|
236
|
+
hasClientIcon || hasExtraIconReferences || pageNeedsDeferredIconRuntime(options.pageData);
|
|
237
|
+
if (!needsManifest) {
|
|
238
|
+
return output;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** @type {Record<string, string>} */
|
|
242
|
+
const manifestIcons = {};
|
|
243
|
+
for (const [key, reference] of iconReferences) {
|
|
244
|
+
const svg = reference.svg || (await resolver.loadIcon(reference.library, reference.name));
|
|
245
|
+
manifestIcons[key] = iconAssetStore.addIcon({
|
|
246
|
+
library: reference.library,
|
|
247
|
+
name: reference.name,
|
|
248
|
+
svg,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
iconAssetStore.needsRuntime = true;
|
|
252
|
+
|
|
253
|
+
return injectIconManifest(output, {
|
|
254
|
+
defaultLibrary: resolver.defaultLibrary,
|
|
255
|
+
icons: manifestIcons,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {unknown} iconLibraries
|
|
261
|
+
* @param {string} owner
|
|
262
|
+
* @returns {Map<string, import('@rocket/js/types.js').NormalizedIconLibraryConfig>}
|
|
263
|
+
*/
|
|
264
|
+
export function normalizeIconLibrariesConfig(iconLibraries, owner) {
|
|
265
|
+
const normalized = new Map();
|
|
266
|
+
if (iconLibraries === undefined) {
|
|
267
|
+
return normalized;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const entries = iconLibraryEntries(iconLibraries, owner);
|
|
271
|
+
for (const [name, config] of entries) {
|
|
272
|
+
if (typeof name !== 'string' || name.trim() === '') {
|
|
273
|
+
throw new Error(`Invalid ${owner}: Icon Library names must be non-empty strings.`);
|
|
274
|
+
}
|
|
275
|
+
if (normalized.has(name)) {
|
|
276
|
+
throw new Error(`Invalid ${owner}: duplicate Icon Library "${name}".`);
|
|
277
|
+
}
|
|
278
|
+
normalized.set(name, {
|
|
279
|
+
sources: normalizeIconLibrarySources(config, `${owner} "${name}"`),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return normalized;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* @param {unknown} iconReferences
|
|
287
|
+
* @param {string} owner
|
|
288
|
+
* @returns {import('@rocket/js/types.js').IconReferenceConfig[]}
|
|
289
|
+
*/
|
|
290
|
+
export function normalizeIconReferencesConfig(iconReferences, owner) {
|
|
291
|
+
if (iconReferences === undefined) {
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
if (!Array.isArray(iconReferences)) {
|
|
295
|
+
throw new Error(`Invalid ${owner}: must be an array.`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** @type {import('@rocket/js/types.js').IconReferenceConfig[]} */
|
|
299
|
+
const normalized = [];
|
|
300
|
+
const seen = new Set();
|
|
301
|
+
for (const [index, iconReference] of iconReferences.entries()) {
|
|
302
|
+
const field = `${owner}[${index}]`;
|
|
303
|
+
if (!isPlainRecord(iconReference)) {
|
|
304
|
+
throw new Error(`Invalid ${field}: must be an object.`);
|
|
305
|
+
}
|
|
306
|
+
for (const key of Object.keys(iconReference)) {
|
|
307
|
+
if (!ICON_REFERENCE_CONFIG_FIELDS.has(key)) {
|
|
308
|
+
throw new Error(`Invalid ${field}.${key}: is not a known Icon Reference field.`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const name = readNonEmptyString(iconReference.name, `${field}.name`).trim();
|
|
313
|
+
const library =
|
|
314
|
+
iconReference.library === undefined
|
|
315
|
+
? undefined
|
|
316
|
+
: readNonEmptyString(iconReference.library, `${field}.library`).trim();
|
|
317
|
+
const dedupeKey = `${library || ''}:${name}`;
|
|
318
|
+
if (!seen.has(dedupeKey)) {
|
|
319
|
+
seen.add(dedupeKey);
|
|
320
|
+
normalized.push({
|
|
321
|
+
...(library ? { library } : {}),
|
|
322
|
+
name,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return normalized;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @typedef {{ remaining?: number }} IconLoadingRegion
|
|
331
|
+
* @typedef {{
|
|
332
|
+
* start: number;
|
|
333
|
+
* end: number;
|
|
334
|
+
* html: string;
|
|
335
|
+
* renderedIcon: Awaited<ReturnType<typeof renderRocketIconHost>>;
|
|
336
|
+
* }} IconHostReplacement
|
|
337
|
+
*/
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* @param {string} html
|
|
341
|
+
* @param {ReturnType<typeof createIconResolver>} resolver
|
|
342
|
+
* @returns {Promise<IconHostReplacement[]>}
|
|
343
|
+
*/
|
|
344
|
+
async function finalizeRocketIconHosts(html, resolver) {
|
|
345
|
+
const document = parse5.parse(html, { sourceCodeLocationInfo: true });
|
|
346
|
+
/** @type {IconHostReplacement[]} */
|
|
347
|
+
const replacements = [];
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* @param {import('parse5').DefaultTreeAdapterTypes.Node} node
|
|
351
|
+
* @param {IconLoadingRegion | undefined} region
|
|
352
|
+
*/
|
|
353
|
+
async function visitNode(node, region) {
|
|
354
|
+
if (!isElementNode(node)) {
|
|
355
|
+
await visitChildren(node, region);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (node.tagName === 'rocket-icon') {
|
|
360
|
+
const replacement = await finalizeRocketIconElement(html, node, resolver, region);
|
|
361
|
+
replacements.push(replacement);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const childRegion = iconLoadingRegionForElement(node) || region;
|
|
366
|
+
await visitChildren(node, childRegion);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* @param {import('parse5').DefaultTreeAdapterTypes.Node} node
|
|
371
|
+
* @param {IconLoadingRegion | undefined} region
|
|
372
|
+
*/
|
|
373
|
+
async function visitChildren(node, region) {
|
|
374
|
+
if (!('childNodes' in node)) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
for (const child of node.childNodes) {
|
|
378
|
+
await visitNode(child, region);
|
|
379
|
+
}
|
|
380
|
+
if (isTemplateElement(node)) {
|
|
381
|
+
await visitNode(node.content, region);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
await visitNode(document, undefined);
|
|
386
|
+
replacements.sort((first, second) => first.start - second.start);
|
|
387
|
+
return replacements;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @param {string} html
|
|
392
|
+
* @param {import('parse5').DefaultTreeAdapterTypes.Element} node
|
|
393
|
+
* @param {ReturnType<typeof createIconResolver>} resolver
|
|
394
|
+
* @param {IconLoadingRegion | undefined} region
|
|
395
|
+
* @returns {Promise<IconHostReplacement>}
|
|
396
|
+
*/
|
|
397
|
+
async function finalizeRocketIconElement(html, node, resolver, region) {
|
|
398
|
+
const location = node.sourceCodeLocation;
|
|
399
|
+
const startTag = location?.startTag;
|
|
400
|
+
if (!location || !startTag) {
|
|
401
|
+
throw new Error('rocket-icon host is missing a source location in rendered HTML.');
|
|
402
|
+
}
|
|
403
|
+
if (!location.endTag) {
|
|
404
|
+
throw new Error('rocket-icon hosts must use an explicit closing </rocket-icon> tag.');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const attributes = attributesForElement(node);
|
|
408
|
+
const effectiveIconLoading = effectiveLoadingForIcon(attributes, region);
|
|
409
|
+
const renderedIcon = await renderRocketIconHost(
|
|
410
|
+
html.slice(startTag.startOffset, startTag.endOffset),
|
|
411
|
+
attributes,
|
|
412
|
+
resolver,
|
|
413
|
+
effectiveIconLoading,
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
start: location.startOffset,
|
|
418
|
+
end: location.endOffset,
|
|
419
|
+
html: renderedIcon.html,
|
|
420
|
+
renderedIcon,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* @param {Map<string, string | true>} attributes
|
|
426
|
+
* @param {IconLoadingRegion | undefined} region
|
|
427
|
+
*/
|
|
428
|
+
function effectiveLoadingForIcon(attributes, region) {
|
|
429
|
+
const iconLoading = iconLoadingForAttributes(attributes);
|
|
430
|
+
if (iconLoading !== 'auto') {
|
|
431
|
+
return iconLoading;
|
|
432
|
+
}
|
|
433
|
+
if (!region || region.remaining === undefined) {
|
|
434
|
+
return 'server';
|
|
435
|
+
}
|
|
436
|
+
if (region.remaining > 0) {
|
|
437
|
+
region.remaining -= 1;
|
|
438
|
+
return 'server';
|
|
439
|
+
}
|
|
440
|
+
return 'client';
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* @param {import('parse5').DefaultTreeAdapterTypes.Element} node
|
|
445
|
+
* @returns {IconLoadingRegion | undefined}
|
|
446
|
+
*/
|
|
447
|
+
function iconLoadingRegionForElement(node) {
|
|
448
|
+
const attributes = attributesForElement(node);
|
|
449
|
+
if (!attributes.has('icon-loading-region')) {
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
const budgetAttribute = attributes.get('icon-server-budget');
|
|
453
|
+
if (budgetAttribute === undefined) {
|
|
454
|
+
return {};
|
|
455
|
+
}
|
|
456
|
+
if (typeof budgetAttribute !== 'string' || !/^\d+$/.test(budgetAttribute)) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
`Invalid icon-server-budget ${JSON.stringify(
|
|
459
|
+
budgetAttribute === true ? '' : budgetAttribute,
|
|
460
|
+
)}. Expected a non-negative integer.`,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
return { remaining: Number(budgetAttribute) };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* @param {import('parse5').DefaultTreeAdapterTypes.Element} node
|
|
468
|
+
* @returns {Map<string, string | true>}
|
|
469
|
+
*/
|
|
470
|
+
function attributesForElement(node) {
|
|
471
|
+
const attributes = new Map();
|
|
472
|
+
for (const attribute of node.attrs) {
|
|
473
|
+
attributes.set(attribute.name.toLowerCase(), attribute.value);
|
|
474
|
+
}
|
|
475
|
+
return attributes;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* @param {import('parse5').DefaultTreeAdapterTypes.Node} node
|
|
480
|
+
* @returns {node is import('parse5').DefaultTreeAdapterTypes.Element}
|
|
481
|
+
*/
|
|
482
|
+
function isElementNode(node) {
|
|
483
|
+
return 'tagName' in node && typeof node.tagName === 'string';
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* @param {import('parse5').DefaultTreeAdapterTypes.Node} node
|
|
488
|
+
* @returns {node is import('parse5').DefaultTreeAdapterTypes.Template}
|
|
489
|
+
*/
|
|
490
|
+
function isTemplateElement(node) {
|
|
491
|
+
return isElementNode(node) && node.tagName === 'template' && 'content' in node;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* @param {string} html
|
|
496
|
+
* @param {IconHostReplacement[]} replacements
|
|
497
|
+
*/
|
|
498
|
+
function applyHtmlReplacements(html, replacements) {
|
|
499
|
+
let output = '';
|
|
500
|
+
let cursor = 0;
|
|
501
|
+
for (const replacement of replacements) {
|
|
502
|
+
output += html.slice(cursor, replacement.start);
|
|
503
|
+
output += replacement.html;
|
|
504
|
+
cursor = replacement.end;
|
|
505
|
+
}
|
|
506
|
+
return output + html.slice(cursor);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* @param {string} startTag
|
|
511
|
+
* @param {Map<string, string | true>} attributes
|
|
512
|
+
* @param {ReturnType<typeof createIconResolver>} resolver
|
|
513
|
+
* @param {string} [effectiveIconLoading]
|
|
514
|
+
*/
|
|
515
|
+
async function renderRocketIconHost(startTag, attributes, resolver, effectiveIconLoading) {
|
|
516
|
+
const name = stringAttribute(attributes, 'name')?.trim();
|
|
517
|
+
if (!name) {
|
|
518
|
+
throw new Error('rocket-icon requires a non-empty name attribute.');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const iconLoading = iconLoadingForAttributes(attributes);
|
|
522
|
+
if (!ICON_LOADING_VALUES.has(iconLoading)) {
|
|
523
|
+
throw new Error(
|
|
524
|
+
`Invalid rocket-icon icon-loading ${JSON.stringify(
|
|
525
|
+
iconLoading,
|
|
526
|
+
)}. Expected "auto", "server", or "client".`,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const libraryAttribute = stringAttribute(attributes, 'library');
|
|
531
|
+
const library = resolver.resolveLibrary(libraryAttribute, name);
|
|
532
|
+
const resolvedIconLoading = effectiveIconLoading || iconLoading;
|
|
533
|
+
const svg = resolvedIconLoading === 'client' ? '' : await resolver.loadIcon(library, name);
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
html: `${startTag}${shadowTemplate(svg)}</rocket-icon>`,
|
|
537
|
+
iconLoading: resolvedIconLoading,
|
|
538
|
+
library,
|
|
539
|
+
name,
|
|
540
|
+
svg,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* @param {{
|
|
546
|
+
* layoutIconLibraries?: unknown;
|
|
547
|
+
* layoutDefaultIconLibrary?: string;
|
|
548
|
+
* projectIconLibraries?: unknown;
|
|
549
|
+
* projectDefaultIconLibrary?: string;
|
|
550
|
+
* }} options
|
|
551
|
+
*/
|
|
552
|
+
function createIconResolver({
|
|
553
|
+
layoutIconLibraries,
|
|
554
|
+
layoutDefaultIconLibrary,
|
|
555
|
+
projectIconLibraries,
|
|
556
|
+
projectDefaultIconLibrary,
|
|
557
|
+
}) {
|
|
558
|
+
const layoutLibraries = normalizeIconLibrariesConfig(
|
|
559
|
+
layoutIconLibraries,
|
|
560
|
+
'Layout Icon Libraries',
|
|
561
|
+
);
|
|
562
|
+
const projectLibraries = normalizeIconLibrariesConfig(
|
|
563
|
+
projectIconLibraries,
|
|
564
|
+
'Project Icon Library Configuration',
|
|
565
|
+
);
|
|
566
|
+
for (const name of projectLibraries.keys()) {
|
|
567
|
+
if (layoutLibraries.has(name)) {
|
|
568
|
+
throw new Error(
|
|
569
|
+
`Icon Library "${name}" is supplied by both project configuration and the active layout.`,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const libraries = new Map([...layoutLibraries, ...projectLibraries]);
|
|
575
|
+
const defaultLibrary = selectDefaultIconLibrary({
|
|
576
|
+
libraries,
|
|
577
|
+
layoutDefaultIconLibrary,
|
|
578
|
+
projectDefaultIconLibrary,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
defaultLibrary,
|
|
583
|
+
libraries,
|
|
584
|
+
/**
|
|
585
|
+
* @param {string | undefined} explicitLibrary
|
|
586
|
+
* @param {string} iconName
|
|
587
|
+
*/
|
|
588
|
+
resolveLibrary(explicitLibrary, iconName) {
|
|
589
|
+
if (explicitLibrary !== undefined) {
|
|
590
|
+
const trimmed = explicitLibrary.trim();
|
|
591
|
+
if (!trimmed) {
|
|
592
|
+
throw new Error(`rocket-icon library must be non-empty when provided.`);
|
|
593
|
+
}
|
|
594
|
+
if (!libraries.has(trimmed)) {
|
|
595
|
+
throw new Error(`Unknown Icon Library "${trimmed}" for rocket-icon "${iconName}".`);
|
|
596
|
+
}
|
|
597
|
+
return trimmed;
|
|
598
|
+
}
|
|
599
|
+
if (defaultLibrary) {
|
|
600
|
+
return defaultLibrary;
|
|
601
|
+
}
|
|
602
|
+
if (libraries.size === 0) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`rocket-icon "${iconName}" has no Icon Library. Configure iconLibraries or provide library.`,
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
throw new Error(
|
|
608
|
+
`Ambiguous unqualified rocket-icon "${iconName}". Provide library or configure defaultIconLibrary.`,
|
|
609
|
+
);
|
|
610
|
+
},
|
|
611
|
+
/**
|
|
612
|
+
* @param {string} library
|
|
613
|
+
* @param {string} iconName
|
|
614
|
+
*/
|
|
615
|
+
async loadIcon(library, iconName) {
|
|
616
|
+
const config = libraries.get(library);
|
|
617
|
+
if (!config) {
|
|
618
|
+
throw new Error(`Unknown Icon Library "${library}" for rocket-icon "${iconName}".`);
|
|
619
|
+
}
|
|
620
|
+
const icons = await indexIconLibrary(library, config);
|
|
621
|
+
const svg = icons.get(iconName);
|
|
622
|
+
if (svg === undefined) {
|
|
623
|
+
throw new Error(`Icon "${iconName}" was not found in Icon Library "${library}".`);
|
|
624
|
+
}
|
|
625
|
+
return svg;
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* @param {object} options
|
|
632
|
+
* @param {Map<string, import('@rocket/js/types.js').NormalizedIconLibraryConfig>} options.libraries
|
|
633
|
+
* @param {string} [options.layoutDefaultIconLibrary]
|
|
634
|
+
* @param {string} [options.projectDefaultIconLibrary]
|
|
635
|
+
*/
|
|
636
|
+
function selectDefaultIconLibrary({
|
|
637
|
+
libraries,
|
|
638
|
+
layoutDefaultIconLibrary,
|
|
639
|
+
projectDefaultIconLibrary,
|
|
640
|
+
}) {
|
|
641
|
+
const configuredDefault = projectDefaultIconLibrary || layoutDefaultIconLibrary;
|
|
642
|
+
if (configuredDefault !== undefined) {
|
|
643
|
+
const defaultName = configuredDefault.trim();
|
|
644
|
+
if (!defaultName) {
|
|
645
|
+
throw new Error('defaultIconLibrary must be a non-empty string.');
|
|
646
|
+
}
|
|
647
|
+
if (!libraries.has(defaultName)) {
|
|
648
|
+
throw new Error(`Default Icon Library "${defaultName}" is not configured.`);
|
|
649
|
+
}
|
|
650
|
+
return defaultName;
|
|
651
|
+
}
|
|
652
|
+
if (libraries.size === 1) {
|
|
653
|
+
return [...libraries.keys()][0];
|
|
654
|
+
}
|
|
655
|
+
return undefined;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* @param {string} libraryName
|
|
660
|
+
* @param {import('@rocket/js/types.js').NormalizedIconLibraryConfig} config
|
|
661
|
+
*/
|
|
662
|
+
async function indexIconLibrary(libraryName, config) {
|
|
663
|
+
const cacheKey = `${libraryName}:${JSON.stringify(config.sources)}`;
|
|
664
|
+
let cached = libraryIndexCache.get(cacheKey);
|
|
665
|
+
if (!cached) {
|
|
666
|
+
cached = readIconLibrary(libraryName, config);
|
|
667
|
+
libraryIndexCache.set(cacheKey, cached);
|
|
668
|
+
}
|
|
669
|
+
return cached;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* @param {string} libraryName
|
|
674
|
+
* @param {import('@rocket/js/types.js').NormalizedIconLibraryConfig} config
|
|
675
|
+
*/
|
|
676
|
+
async function readIconLibrary(libraryName, config) {
|
|
677
|
+
/** @type {Map<string, string>} */
|
|
678
|
+
const icons = new Map();
|
|
679
|
+
for (const source of config.sources) {
|
|
680
|
+
const files = await iconSourceFiles(source);
|
|
681
|
+
if (files.length === 0) {
|
|
682
|
+
throw new Error(
|
|
683
|
+
`Icon Library "${libraryName}" source ${JSON.stringify(source.files)} matched no SVG files.`,
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
for (const file of files) {
|
|
687
|
+
const iconName = path.basename(file, '.svg');
|
|
688
|
+
if (icons.has(iconName)) {
|
|
689
|
+
throw new Error(`Duplicate Icon Name "${iconName}" in Icon Library "${libraryName}".`);
|
|
690
|
+
}
|
|
691
|
+
icons.set(iconName, await readFile(file, 'utf8'));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return icons;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* @param {import('@rocket/js/types.js').IconLibrarySource} source
|
|
699
|
+
* @returns {Promise<string[]>}
|
|
700
|
+
*/
|
|
701
|
+
async function iconSourceFiles(source) {
|
|
702
|
+
const pattern =
|
|
703
|
+
source.type === 'package'
|
|
704
|
+
? path.join(packageRoot(source.packageName), source.files)
|
|
705
|
+
: path.resolve(source.files);
|
|
706
|
+
const files = [];
|
|
707
|
+
for await (const file of glob(pattern)) {
|
|
708
|
+
if (file.endsWith('.svg')) {
|
|
709
|
+
files.push(path.resolve(file));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
files.sort();
|
|
713
|
+
return files;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* @param {string} packageName
|
|
718
|
+
*/
|
|
719
|
+
function packageRoot(packageName) {
|
|
720
|
+
try {
|
|
721
|
+
return path.dirname(require.resolve(`${packageName}/package.json`, { paths: [process.cwd()] }));
|
|
722
|
+
} catch (error) {
|
|
723
|
+
throw new Error(`Could not resolve Icon package ${JSON.stringify(packageName)}.`, {
|
|
724
|
+
cause: error,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* @param {unknown} iconLibraries
|
|
731
|
+
* @param {string} owner
|
|
732
|
+
* @returns {[string, unknown][]}
|
|
733
|
+
*/
|
|
734
|
+
function iconLibraryEntries(iconLibraries, owner) {
|
|
735
|
+
if (iconLibraries instanceof Map) {
|
|
736
|
+
return [...iconLibraries.entries()];
|
|
737
|
+
}
|
|
738
|
+
if (!isPlainRecord(iconLibraries)) {
|
|
739
|
+
throw new Error(`Invalid ${owner}: iconLibraries must be an object.`);
|
|
740
|
+
}
|
|
741
|
+
return Object.entries(iconLibraries);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* @param {unknown} config
|
|
746
|
+
* @param {string} field
|
|
747
|
+
* @returns {import('@rocket/js/types.js').IconLibrarySource[]}
|
|
748
|
+
*/
|
|
749
|
+
function normalizeIconLibrarySources(config, field) {
|
|
750
|
+
if (isIconLibrarySource(config)) {
|
|
751
|
+
return [normalizeIconLibrarySource(config, field)];
|
|
752
|
+
}
|
|
753
|
+
if (isPlainRecord(config) && Object.prototype.hasOwnProperty.call(config, 'type')) {
|
|
754
|
+
return [normalizeIconLibrarySource(config, field)];
|
|
755
|
+
}
|
|
756
|
+
if (Array.isArray(config)) {
|
|
757
|
+
return config.map(source => normalizeIconLibrarySource(source, field));
|
|
758
|
+
}
|
|
759
|
+
if (isPlainRecord(config) && Object.prototype.hasOwnProperty.call(config, 'sources')) {
|
|
760
|
+
return normalizeIconLibrarySources(config.sources, `${field}.sources`);
|
|
761
|
+
}
|
|
762
|
+
throw new Error(
|
|
763
|
+
`Invalid ${field}: expected an Icon Library Source, an array of sources, or { sources } configuration.`,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* @param {unknown} source
|
|
769
|
+
* @param {string} field
|
|
770
|
+
* @returns {import('@rocket/js/types.js').IconLibrarySource}
|
|
771
|
+
*/
|
|
772
|
+
function normalizeIconLibrarySource(source, field) {
|
|
773
|
+
if (!isPlainRecord(source)) {
|
|
774
|
+
throw new Error(`Invalid ${field}: Icon Library Sources must be objects.`);
|
|
775
|
+
}
|
|
776
|
+
if (source.type === 'package') {
|
|
777
|
+
return iconsFromPackage(
|
|
778
|
+
readNonEmptyString(source.packageName, `${field}.packageName`),
|
|
779
|
+
readNonEmptyString(source.files, `${field}.files`),
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
if (source.type === 'path') {
|
|
783
|
+
return iconsFromPath(readNonEmptyString(source.files, `${field}.files`));
|
|
784
|
+
}
|
|
785
|
+
throw new Error(`Invalid ${field}: Icon Library Source type must be "package" or "path".`);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* @param {unknown} value
|
|
790
|
+
* @returns {value is import('@rocket/js/types.js').IconLibrarySource}
|
|
791
|
+
*/
|
|
792
|
+
function isIconLibrarySource(value) {
|
|
793
|
+
return isPlainRecord(value) && (value.type === 'package' || value.type === 'path');
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* @param {string} svg
|
|
798
|
+
*/
|
|
799
|
+
function shadowTemplate(svg) {
|
|
800
|
+
return `<template shadowrootmode="open"><style>${SHADOW_ICON_STYLE}</style><span part="icon">${svg}</span></template>`;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* @param {string} library
|
|
805
|
+
* @param {string} name
|
|
806
|
+
*/
|
|
807
|
+
function iconReferenceKey(library, name) {
|
|
808
|
+
return `${library}:${name}`;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* @param {{ library: string; name: string; svg: string }} icon
|
|
813
|
+
*/
|
|
814
|
+
function iconAssetUrl({ library, name, svg }) {
|
|
815
|
+
const hash = createHash('sha256').update(svg).digest('hex').slice(0, 12);
|
|
816
|
+
return `${ROCKET_ICON_ASSET_PREFIX}${sanitizePathSegment(library)}/${sanitizePathSegment(
|
|
817
|
+
name,
|
|
818
|
+
)}.${hash}.svg`;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* @param {string} html
|
|
823
|
+
* @param {{ defaultLibrary?: string; icons: Record<string, string> }} manifest
|
|
824
|
+
*/
|
|
825
|
+
function injectIconManifest(html, manifest) {
|
|
826
|
+
const manifestJson = JSON.stringify(
|
|
827
|
+
manifest.defaultLibrary
|
|
828
|
+
? { defaultLibrary: manifest.defaultLibrary, icons: manifest.icons }
|
|
829
|
+
: { icons: manifest.icons },
|
|
830
|
+
).replaceAll('<', '\\u003C');
|
|
831
|
+
const runtimeHtml =
|
|
832
|
+
`<script type="application/json" data-rocket-icon-manifest>${manifestJson}</script>` +
|
|
833
|
+
`<script type="module" data-rocket-icon-runtime>import '${ROCKET_ICON_DEFINE_MODULE_PATH}';</script>`;
|
|
834
|
+
const headClose = html.toLowerCase().indexOf('</head>');
|
|
835
|
+
if (headClose !== -1) {
|
|
836
|
+
return html.slice(0, headClose) + runtimeHtml + html.slice(headClose);
|
|
837
|
+
}
|
|
838
|
+
return html + runtimeHtml;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* @param {{ _hydrationScript?: string; _hasBrowserLoadedComponents?: boolean } | undefined} pageData
|
|
843
|
+
*/
|
|
844
|
+
function pageNeedsDeferredIconRuntime(pageData) {
|
|
845
|
+
return Boolean(pageData?._hasBrowserLoadedComponents || pageData?._hydrationScript);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* @param {string} value
|
|
850
|
+
*/
|
|
851
|
+
function sanitizePathSegment(value) {
|
|
852
|
+
const sanitized = value
|
|
853
|
+
.trim()
|
|
854
|
+
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
855
|
+
.replace(/^-+|-+$/g, '');
|
|
856
|
+
return sanitized || 'icon';
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* @param {Map<string, string | true>} attributes
|
|
861
|
+
* @param {string} name
|
|
862
|
+
*/
|
|
863
|
+
function stringAttribute(attributes, name) {
|
|
864
|
+
const value = attributes.get(name);
|
|
865
|
+
return typeof value === 'string' ? value : undefined;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* @param {Map<string, string | true>} attributes
|
|
870
|
+
*/
|
|
871
|
+
function iconLoadingForAttributes(attributes) {
|
|
872
|
+
return attributes.has('icon-loading')
|
|
873
|
+
? stringAttribute(attributes, 'icon-loading') || ''
|
|
874
|
+
: 'auto';
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* @param {unknown} value
|
|
879
|
+
* @param {string} field
|
|
880
|
+
*/
|
|
881
|
+
function readNonEmptyString(value, field) {
|
|
882
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
883
|
+
throw new Error(`${field} must be a non-empty string.`);
|
|
884
|
+
}
|
|
885
|
+
return value;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* @param {unknown} value
|
|
890
|
+
* @returns {value is Record<string, unknown>}
|
|
891
|
+
*/
|
|
892
|
+
function isPlainRecord(value) {
|
|
893
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
const prototype = Object.getPrototypeOf(value);
|
|
897
|
+
return prototype === Object.prototype || prototype === null;
|
|
898
|
+
}
|