@scaffold-ui-kit/preview 0.0.0-stage → 2.3.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/LICENSE +21 -0
- package/README.md +1868 -2
- package/application/host/bootstrap.ts +43 -0
- package/application/host/main.ts +31 -0
- package/application/host/shell-host.ts +47 -0
- package/application/index.html +12 -0
- package/application/lib/base-url.ts +15 -0
- package/application/lib/no-code-block.ts +7 -0
- package/application/lib/no-setup.ts +3 -0
- package/application/lib/no-views.ts +6 -0
- package/application/preview/code/code.html +12 -0
- package/application/preview/code/main.ts +89 -0
- package/application/preview/globals.ts +254 -0
- package/application/preview/html/main.ts +93 -0
- package/application/preview/html/preview.html +12 -0
- package/application/preview/react/main.tsx +146 -0
- package/application/preview/react/preview.html +12 -0
- package/application/preview/svelte/Preview.svelte +104 -0
- package/application/preview/svelte/main.ts +13 -0
- package/application/preview/svelte/preview.html +12 -0
- package/application/preview/svelte/story-props.ts +172 -0
- package/application/preview/view/main.ts +124 -0
- package/application/preview/view/view.html +12 -0
- package/application/preview/vue/main.ts +145 -0
- package/application/preview/vue/preview.html +12 -0
- package/application/shells/charcell/components/Application/index.css +37 -0
- package/application/shells/charcell/components/Application/index.tsx +65 -0
- package/application/shells/charcell/components/Bar/index.css +68 -0
- package/application/shells/charcell/components/Bar/index.tsx +60 -0
- package/application/shells/charcell/components/CodePane/index.css +60 -0
- package/application/shells/charcell/components/CodePane/index.tsx +113 -0
- package/application/shells/charcell/components/ComponentStage/index.css +26 -0
- package/application/shells/charcell/components/ComponentStage/index.tsx +110 -0
- package/application/shells/charcell/components/Control/index.css +20 -0
- package/application/shells/charcell/components/Control/index.tsx +118 -0
- package/application/shells/charcell/components/CopyLink/index.tsx +35 -0
- package/application/shells/charcell/components/Description/index.css +6 -0
- package/application/shells/charcell/components/Description/index.tsx +24 -0
- package/application/shells/charcell/components/EditorAddDialog/index.css +9 -0
- package/application/shells/charcell/components/EditorAddDialog/index.tsx +158 -0
- package/application/shells/charcell/components/EditorPanel/index.css +48 -0
- package/application/shells/charcell/components/EditorPanel/index.tsx +496 -0
- package/application/shells/charcell/components/EventsRow/index.css +42 -0
- package/application/shells/charcell/components/EventsRow/index.tsx +68 -0
- package/application/shells/charcell/components/HeadingRow/index.css +11 -0
- package/application/shells/charcell/components/HeadingRow/index.tsx +16 -0
- package/application/shells/charcell/components/Hint/index.css +16 -0
- package/application/shells/charcell/components/Hint/index.tsx +81 -0
- package/application/shells/charcell/components/HtmlWarnings/index.tsx +45 -0
- package/application/shells/charcell/components/KeyHints/index.css +27 -0
- package/application/shells/charcell/components/KeyHints/index.tsx +106 -0
- package/application/shells/charcell/components/KitCodeBlock/index.css +55 -0
- package/application/shells/charcell/components/KitCodeBlock/index.tsx +18 -0
- package/application/shells/charcell/components/Markdown/index.tsx +16 -0
- package/application/shells/charcell/components/Navigation/index.css +13 -0
- package/application/shells/charcell/components/Navigation/index.tsx +98 -0
- package/application/shells/charcell/components/NavigationSplit/index.css +16 -0
- package/application/shells/charcell/components/NavigationSplit/index.tsx +82 -0
- package/application/shells/charcell/components/OptionControls/index.tsx +93 -0
- package/application/shells/charcell/components/PaneStatus/index.css +7 -0
- package/application/shells/charcell/components/PaneStatus/index.tsx +47 -0
- package/application/shells/charcell/components/PaneWidth/index.tsx +45 -0
- package/application/shells/charcell/components/PreviewGrid/index.css +39 -0
- package/application/shells/charcell/components/PreviewGrid/index.tsx +344 -0
- package/application/shells/charcell/components/PreviewPane/index.css +127 -0
- package/application/shells/charcell/components/PreviewPane/index.tsx +182 -0
- package/application/shells/charcell/components/PromptBar/index.tsx +133 -0
- package/application/shells/charcell/components/ScenarioRow/index.css +36 -0
- package/application/shells/charcell/components/ScenarioRow/index.tsx +71 -0
- package/application/shells/charcell/components/Select/index.tsx +48 -0
- package/application/shells/charcell/components/SlotOutlet/index.css +7 -0
- package/application/shells/charcell/components/SlotOutlet/index.tsx +25 -0
- package/application/shells/charcell/components/StageBar/index.css +7 -0
- package/application/shells/charcell/components/StageBar/index.tsx +109 -0
- package/application/shells/charcell/components/StageSplit/index.css +11 -0
- package/application/shells/charcell/components/StageSplit/index.tsx +99 -0
- package/application/shells/charcell/components/StatusBar/index.tsx +30 -0
- package/application/shells/charcell/components/ViewControls/index.css +15 -0
- package/application/shells/charcell/components/ViewControls/index.tsx +58 -0
- package/application/shells/charcell/components/ViewHeading/index.css +13 -0
- package/application/shells/charcell/components/ViewHeading/index.tsx +8 -0
- package/application/shells/charcell/components/ViewStage/index.css +9 -0
- package/application/shells/charcell/components/ViewStage/index.tsx +25 -0
- package/application/shells/charcell/components/WrapToggle/index.tsx +27 -0
- package/application/shells/charcell/configuration.ts +45 -0
- package/application/shells/charcell/controller-context.ts +27 -0
- package/application/shells/charcell/fit-labels.ts +42 -0
- package/application/shells/charcell/html-warnings.ts +18 -0
- package/application/shells/charcell/index.tsx +82 -0
- package/application/shells/charcell/page-loading.html +57 -0
- package/application/shells/charcell/placement.ts +24 -0
- package/application/shells/charcell/shell-options.ts +23 -0
- package/application/shells/charcell/shell.css +37 -0
- package/application/shells/charcell/shell.tsx +25 -0
- package/application/shells/charcell/split-reset.ts +47 -0
- package/application/shells/charcell/stacked-rows.ts +117 -0
- package/application/shells/charcell/stage-toggles.ts +93 -0
- package/application/types/manifest.d.ts +6 -0
- package/application/types/preview-configuration.d.ts +10 -0
- package/application/types/preview-shell.d.ts +7 -0
- package/application/types/react-registry.d.ts +15 -0
- package/application/types/vue-registry.d.ts +6 -0
- package/dist/build.d.ts +24 -0
- package/dist/build.js +34 -0
- package/dist/check.d.ts +48 -0
- package/dist/check.js +781 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +88 -0
- package/dist/context.d.ts +179 -0
- package/dist/context.js +783 -0
- package/dist/controller/catalog.d.ts +339 -0
- package/dist/controller/catalog.js +367 -0
- package/dist/controller/create-preview-controller.d.ts +532 -0
- package/dist/controller/create-preview-controller.js +1707 -0
- package/dist/controller/editor-tree.d.ts +161 -0
- package/dist/controller/editor-tree.js +373 -0
- package/dist/controller/html-source.d.ts +62 -0
- package/dist/controller/html-source.js +61 -0
- package/dist/controller/index.d.ts +9 -0
- package/dist/controller/index.js +12 -0
- package/dist/controller/persistence.d.ts +191 -0
- package/dist/controller/persistence.js +315 -0
- package/dist/controller/regeneration.d.ts +30 -0
- package/dist/controller/regeneration.js +27 -0
- package/dist/controller/source.d.ts +80 -0
- package/dist/controller/source.js +236 -0
- package/dist/controller/store.d.ts +27 -0
- package/dist/controller/store.js +40 -0
- package/dist/declarations/application/shells/charcell/components/Application/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/Bar/index.d.ts +11 -0
- package/dist/declarations/application/shells/charcell/components/CodePane/index.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/components/ComponentStage/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/Control/index.d.ts +9 -0
- package/dist/declarations/application/shells/charcell/components/CopyLink/index.d.ts +1 -0
- package/dist/declarations/application/shells/charcell/components/Description/index.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/components/EditorAddDialog/index.d.ts +6 -0
- package/dist/declarations/application/shells/charcell/components/EditorPanel/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/EventsRow/index.d.ts +4 -0
- package/dist/declarations/application/shells/charcell/components/HeadingRow/index.d.ts +6 -0
- package/dist/declarations/application/shells/charcell/components/Hint/index.d.ts +11 -0
- package/dist/declarations/application/shells/charcell/components/HtmlWarnings/index.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/components/KeyHints/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/KitCodeBlock/index.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/components/Markdown/index.d.ts +3 -0
- package/dist/declarations/application/shells/charcell/components/Navigation/index.d.ts +8 -0
- package/dist/declarations/application/shells/charcell/components/NavigationSplit/index.d.ts +7 -0
- package/dist/declarations/application/shells/charcell/components/OptionControls/index.d.ts +7 -0
- package/dist/declarations/application/shells/charcell/components/PaneStatus/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/PaneWidth/index.d.ts +4 -0
- package/dist/declarations/application/shells/charcell/components/PreviewGrid/index.d.ts +7 -0
- package/dist/declarations/application/shells/charcell/components/PreviewPane/index.d.ts +9 -0
- package/dist/declarations/application/shells/charcell/components/PromptBar/index.d.ts +1 -0
- package/dist/declarations/application/shells/charcell/components/ScenarioRow/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/Select/index.d.ts +16 -0
- package/dist/declarations/application/shells/charcell/components/SlotOutlet/index.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/components/StageBar/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/StageSplit/index.d.ts +17 -0
- package/dist/declarations/application/shells/charcell/components/StatusBar/index.d.ts +1 -0
- package/dist/declarations/application/shells/charcell/components/ViewControls/index.d.ts +9 -0
- package/dist/declarations/application/shells/charcell/components/ViewHeading/index.d.ts +4 -0
- package/dist/declarations/application/shells/charcell/components/ViewStage/index.d.ts +4 -0
- package/dist/declarations/application/shells/charcell/components/WrapToggle/index.d.ts +1 -0
- package/dist/declarations/application/shells/charcell/configuration.d.ts +31 -0
- package/dist/declarations/application/shells/charcell/controller-context.d.ts +9 -0
- package/dist/declarations/application/shells/charcell/fit-labels.d.ts +18 -0
- package/dist/declarations/application/shells/charcell/html-warnings.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/index.d.ts +39 -0
- package/dist/declarations/application/shells/charcell/placement.d.ts +7 -0
- package/dist/declarations/application/shells/charcell/shell-options.d.ts +15 -0
- package/dist/declarations/application/shells/charcell/shell.d.ts +9 -0
- package/dist/declarations/application/shells/charcell/split-reset.d.ts +30 -0
- package/dist/declarations/application/shells/charcell/stacked-rows.d.ts +58 -0
- package/dist/declarations/application/shells/charcell/stage-toggles.d.ts +28 -0
- package/dist/declarations/src/controller/catalog.d.ts +339 -0
- package/dist/declarations/src/controller/create-preview-controller.d.ts +532 -0
- package/dist/declarations/src/controller/editor-tree.d.ts +161 -0
- package/dist/declarations/src/controller/html-source.d.ts +62 -0
- package/dist/declarations/src/controller/index.d.ts +9 -0
- package/dist/declarations/src/controller/persistence.d.ts +191 -0
- package/dist/declarations/src/controller/regeneration.d.ts +30 -0
- package/dist/declarations/src/controller/source.d.ts +80 -0
- package/dist/declarations/src/controller/store.d.ts +27 -0
- package/dist/declarations/src/emit/common.d.ts +103 -0
- package/dist/declarations/src/emit/manifest.d.ts +138 -0
- package/dist/declarations/src/emit/source.d.ts +87 -0
- package/dist/declarations/src/lib/jsx-roots.d.ts +22 -0
- package/dist/declarations/src/lib/module-of-source.d.ts +18 -0
- package/dist/declarations/src/lib/naming.d.ts +29 -0
- package/dist/declarations/src/lib/react-spelling.d.ts +68 -0
- package/dist/declarations/src/lib/scenario-story.d.ts +41 -0
- package/dist/declarations/src/lib/slot-fill.d.ts +15 -0
- package/dist/declarations/src/lib/types.d.ts +741 -0
- package/dist/declarations/src/options.d.ts +224 -0
- package/dist/declarations/src/protocol.d.ts +541 -0
- package/dist/declarations/src/react-view.d.ts +22 -0
- package/dist/declarations/src/react.d.ts +17 -0
- package/dist/declarations/src/shell.d.ts +99 -0
- package/dist/declarations/src/views.d.ts +152 -0
- package/dist/development-server.d.ts +121 -0
- package/dist/development-server.js +331 -0
- package/dist/emit/common.d.ts +103 -0
- package/dist/emit/common.js +170 -0
- package/dist/emit/html-source.d.ts +46 -0
- package/dist/emit/html-source.js +322 -0
- package/dist/emit/manifest.d.ts +138 -0
- package/dist/emit/manifest.js +74 -0
- package/dist/emit/react.d.ts +16 -0
- package/dist/emit/react.js +210 -0
- package/dist/emit/scratch.d.ts +40 -0
- package/dist/emit/scratch.js +116 -0
- package/dist/emit/source.d.ts +87 -0
- package/dist/emit/source.js +789 -0
- package/dist/emit/svelte.d.ts +9 -0
- package/dist/emit/svelte.js +177 -0
- package/dist/emit/vue.d.ts +2 -0
- package/dist/emit/vue.js +152 -0
- package/dist/enumerate.d.ts +2 -0
- package/dist/enumerate.js +162 -0
- package/dist/generate.d.ts +43 -0
- package/dist/generate.js +302 -0
- package/dist/html-stylesheet.d.ts +43 -0
- package/dist/html-stylesheet.js +145 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +21 -0
- package/dist/landing.d.ts +16 -0
- package/dist/landing.js +45 -0
- package/dist/lib/exported-types.d.ts +42 -0
- package/dist/lib/exported-types.js +86 -0
- package/dist/lib/html-attributes.d.ts +2 -0
- package/dist/lib/html-attributes.js +111 -0
- package/dist/lib/html-warnings.d.ts +19 -0
- package/dist/lib/html-warnings.js +79 -0
- package/dist/lib/jsx-roots.d.ts +22 -0
- package/dist/lib/jsx-roots.js +243 -0
- package/dist/lib/module-of-source.d.ts +18 -0
- package/dist/lib/module-of-source.js +24 -0
- package/dist/lib/naming.d.ts +29 -0
- package/dist/lib/naming.js +56 -0
- package/dist/lib/parse-type.d.ts +32 -0
- package/dist/lib/parse-type.js +103 -0
- package/dist/lib/react-spelling.d.ts +68 -0
- package/dist/lib/react-spelling.js +197 -0
- package/dist/lib/scenario-story.d.ts +41 -0
- package/dist/lib/scenario-story.js +84 -0
- package/dist/lib/slot-fill.d.ts +15 -0
- package/dist/lib/slot-fill.js +92 -0
- package/dist/lib/template-files.d.ts +14 -0
- package/dist/lib/template-files.js +25 -0
- package/dist/lib/types.d.ts +741 -0
- package/dist/lib/types.js +13 -0
- package/dist/lib/void-elements.d.ts +7 -0
- package/dist/lib/void-elements.js +13 -0
- package/dist/load.d.ts +37 -0
- package/dist/load.js +82 -0
- package/dist/model.d.ts +44 -0
- package/dist/model.js +464 -0
- package/dist/options.d.ts +224 -0
- package/dist/options.js +223 -0
- package/dist/prop-types.d.ts +26 -0
- package/dist/prop-types.js +102 -0
- package/dist/props-from.d.ts +84 -0
- package/dist/props-from.js +510 -0
- package/dist/protocol.d.ts +541 -0
- package/dist/protocol.js +819 -0
- package/dist/react-view.d.ts +22 -0
- package/dist/react-view.js +36 -0
- package/dist/react.d.ts +17 -0
- package/dist/react.js +23 -0
- package/dist/sass-importers.d.ts +26 -0
- package/dist/sass-importers.js +32 -0
- package/dist/shell-acceptance.d.ts +21 -0
- package/dist/shell-acceptance.js +37 -0
- package/dist/shell-page.d.ts +36 -0
- package/dist/shell-page.js +90 -0
- package/dist/shell-peers.d.ts +38 -0
- package/dist/shell-peers.js +61 -0
- package/dist/shell.d.ts +99 -0
- package/dist/shell.js +52 -0
- package/dist/shells/charcell/page.d.ts +8 -0
- package/dist/shells/charcell/page.js +69 -0
- package/dist/story-source.d.ts +113 -0
- package/dist/story-source.js +271 -0
- package/dist/story.d.ts +5 -0
- package/dist/story.js +28 -0
- package/dist/testing.d.ts +218 -0
- package/dist/testing.js +284 -0
- package/dist/views.d.ts +152 -0
- package/dist/views.js +7 -0
- package/dist/vite-configuration.d.ts +114 -0
- package/dist/vite-configuration.js +578 -0
- package/package.json +123 -3
- package/src/build.ts +51 -0
- package/src/check.ts +1062 -0
- package/src/cli.ts +123 -0
- package/src/context.ts +1168 -0
- package/src/controller/catalog.ts +719 -0
- package/src/controller/create-preview-controller.ts +2683 -0
- package/src/controller/editor-tree.ts +530 -0
- package/src/controller/html-source.ts +114 -0
- package/src/controller/index.ts +105 -0
- package/src/controller/persistence.ts +403 -0
- package/src/controller/regeneration.ts +55 -0
- package/src/controller/source.ts +333 -0
- package/src/controller/store.ts +55 -0
- package/src/development-server.ts +442 -0
- package/src/emit/common.ts +201 -0
- package/src/emit/html-source.ts +402 -0
- package/src/emit/manifest.ts +241 -0
- package/src/emit/react.ts +263 -0
- package/src/emit/scratch.ts +175 -0
- package/src/emit/source.ts +1094 -0
- package/src/emit/svelte.ts +230 -0
- package/src/emit/vue.ts +194 -0
- package/src/enumerate.ts +220 -0
- package/src/generate.ts +443 -0
- package/src/html-stylesheet.ts +173 -0
- package/src/index.ts +60 -0
- package/src/landing.ts +58 -0
- package/src/lib/exported-types.ts +114 -0
- package/src/lib/html-attributes.ts +113 -0
- package/src/lib/html-warnings.ts +81 -0
- package/src/lib/jsx-roots.ts +238 -0
- package/src/lib/module-of-source.ts +26 -0
- package/src/lib/naming.ts +65 -0
- package/src/lib/parse-type.ts +143 -0
- package/src/lib/react-spelling.ts +235 -0
- package/src/lib/scenario-story.ts +97 -0
- package/src/lib/slot-fill.ts +102 -0
- package/src/lib/template-files.ts +31 -0
- package/src/lib/types.ts +778 -0
- package/src/lib/void-elements.ts +15 -0
- package/src/load.ts +144 -0
- package/src/model.ts +568 -0
- package/src/options.ts +435 -0
- package/src/prop-types.ts +133 -0
- package/src/props-from.ts +678 -0
- package/src/protocol.ts +1135 -0
- package/src/react-view.ts +45 -0
- package/src/react.ts +34 -0
- package/src/sass-importers.ts +45 -0
- package/src/shell-acceptance.ts +45 -0
- package/src/shell-page.ts +133 -0
- package/src/shell-peers.ts +70 -0
- package/src/shell.ts +194 -0
- package/src/shells/charcell/page.ts +82 -0
- package/src/story-source.ts +391 -0
- package/src/story.ts +42 -0
- package/src/testing.ts +497 -0
- package/src/views.ts +183 -0
- package/src/vite-configuration.ts +675 -0
package/dist/check.js
ADDED
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
// The preview check: everything in a kit's story files and manual specs that the
|
|
2
|
+
// generator would otherwise take on trust - prop names, enum values, slot names,
|
|
3
|
+
// fill components, story files with no component - checked against what the
|
|
4
|
+
// components actually declare, plus scenario coverage of every enum value and
|
|
5
|
+
// boolean, the kit's preview options and the option values its scenarios
|
|
6
|
+
// set, the packages the charcell shell needs installed, and what the kit's
|
|
7
|
+
// own build reports for its components. Run as
|
|
8
|
+
// `scaffold-ui-kit-preview check` or from a kit's own test suite.
|
|
9
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import { REACT_ATTRIBUTE_NAMES } from '@js-template-engine/extension-react';
|
|
14
|
+
import { loadPreviewContext, } from './context.js';
|
|
15
|
+
import { kitPaneWidths, loadKit } from './load.js';
|
|
16
|
+
import { landingProblems } from './landing.js';
|
|
17
|
+
import { resolveStory, generatedScenarioId, generatesProp, scenarioValues, } from './model.js';
|
|
18
|
+
import { isStructurallyOmitted } from './story.js';
|
|
19
|
+
import { acceptsOptionValue, optionProblems, } from './options.js';
|
|
20
|
+
import { isHtmlAttribute } from './lib/html-attributes.js';
|
|
21
|
+
import { missingShellPeers, missingShellPeersProblem, resolvesFrom, } from './shell-peers.js';
|
|
22
|
+
import { HOST_PAGE, loadShellPage } from './shell-page.js';
|
|
23
|
+
import { moduleOfSource, TARGET_TOKEN } from './lib/module-of-source.js';
|
|
24
|
+
import { parseType, unionMembers } from './lib/parse-type.js';
|
|
25
|
+
const STORY_FILE_SUFFIX = '.story.ts';
|
|
26
|
+
function format(value) {
|
|
27
|
+
return typeof value === 'string' ? `'${value}'` : JSON.stringify(value);
|
|
28
|
+
}
|
|
29
|
+
/** The HTML spelling of each attribute name React spells its own way
|
|
30
|
+
* (`autoComplete` → `autocomplete`, `className` → `class`). */
|
|
31
|
+
const HTML_ATTRIBUTE_NAMES = new Map([
|
|
32
|
+
['className', 'class'],
|
|
33
|
+
...Object.entries(REACT_ATTRIBUTE_NAMES).map(([html, react]) => [react, html]),
|
|
34
|
+
]);
|
|
35
|
+
/**
|
|
36
|
+
* Checks the components a fill tree composes against their declarations: an
|
|
37
|
+
* `element` fill's props against the component's props (its default slot's
|
|
38
|
+
* `children` and, where its root forwards them, native attributes among
|
|
39
|
+
* them) and its `slots` against the named slots it declares, at every depth.
|
|
40
|
+
* An unknown component is reported once, from the story's references.
|
|
41
|
+
*/
|
|
42
|
+
function checkFillTree(name, where, fill, knownComponents, errors) {
|
|
43
|
+
if (!fill)
|
|
44
|
+
return;
|
|
45
|
+
if (fill.kind === 'fragment') {
|
|
46
|
+
for (const child of fill.children)
|
|
47
|
+
checkFillTree(name, where, child, knownComponents, errors);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (fill.kind === 'html') {
|
|
51
|
+
checkFillTree(name, where, fill.children, knownComponents, errors);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (fill.kind !== 'element')
|
|
55
|
+
return;
|
|
56
|
+
const composed = knownComponents.get(fill.component);
|
|
57
|
+
if (composed) {
|
|
58
|
+
const propNames = new Set([
|
|
59
|
+
...Object.keys(composed.props),
|
|
60
|
+
...(composed.hasDefaultSlot ? ['children'] : []),
|
|
61
|
+
]);
|
|
62
|
+
const forwardsAttributes = Boolean(composed.passthrough || composed.rest);
|
|
63
|
+
for (const prop of Object.keys(fill.props ?? {})) {
|
|
64
|
+
if (!propNames.has(prop) &&
|
|
65
|
+
!(forwardsAttributes && isHtmlAttribute(prop))) {
|
|
66
|
+
const html = HTML_ATTRIBUTE_NAMES.get(prop);
|
|
67
|
+
errors.push(html !== undefined &&
|
|
68
|
+
(propNames.has(html) ||
|
|
69
|
+
(forwardsAttributes && isHtmlAttribute(html)))
|
|
70
|
+
? `${name}: ${where} gives ${fill.component} unknown prop '${prop}'; a story spells native attributes as HTML does: '${html}'`
|
|
71
|
+
: `${name}: ${where} gives ${fill.component} unknown prop '${prop}'`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const slotNames = new Set([
|
|
75
|
+
...composed.slotNames,
|
|
76
|
+
...(composed.memberNames ?? []),
|
|
77
|
+
]);
|
|
78
|
+
for (const slot of Object.keys(fill.slots ?? {})) {
|
|
79
|
+
if (!slotNames.has(slot))
|
|
80
|
+
errors.push(`${name}: ${where} fills named slot '${slot}' of ${fill.component}, which it lacks`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
checkFillTree(name, where, fill.children, knownComponents, errors);
|
|
84
|
+
for (const slotFill of Object.values(fill.slots ?? {}))
|
|
85
|
+
checkFillTree(name, where, slotFill, knownComponents, errors);
|
|
86
|
+
}
|
|
87
|
+
/** Checks one component's story file against its declared props and slots. */
|
|
88
|
+
function checkComponent({ component, story }, model, knownComponents, hints, errors, warnings) {
|
|
89
|
+
if (!story)
|
|
90
|
+
return;
|
|
91
|
+
const name = component.name;
|
|
92
|
+
const propNames = new Set(Object.keys(component.props));
|
|
93
|
+
// `children` is the default slot's text, which defaultProps and scenarios may set.
|
|
94
|
+
const valueNames = new Set([
|
|
95
|
+
...propNames,
|
|
96
|
+
...(component.hasDefaultSlot ? ['children'] : []),
|
|
97
|
+
]);
|
|
98
|
+
const fixed = story.fixed ?? {};
|
|
99
|
+
// A `passthrough` root or a `rest` spread takes the consumer's native
|
|
100
|
+
// attributes beside the declared props.
|
|
101
|
+
const forwardsAttributes = Boolean(component.passthrough || component.rest);
|
|
102
|
+
for (const prop of Object.keys(story.controls ?? {})) {
|
|
103
|
+
if (!propNames.has(prop))
|
|
104
|
+
errors.push(`${name}: controls names unknown prop '${prop}'`);
|
|
105
|
+
}
|
|
106
|
+
// Every place a story file sets prop values, checked for names and enum values.
|
|
107
|
+
const valueSources = [
|
|
108
|
+
{ where: 'defaultProps', props: story.defaultProps ?? {} },
|
|
109
|
+
{ where: 'fixed', props: fixed },
|
|
110
|
+
...(story.scenarios ?? []).map((scenario) => ({
|
|
111
|
+
where: `scenario '${scenario.id}'`,
|
|
112
|
+
props: scenario.props ?? {},
|
|
113
|
+
})),
|
|
114
|
+
];
|
|
115
|
+
for (const { where, props } of valueSources) {
|
|
116
|
+
for (const [prop, value] of Object.entries(props)) {
|
|
117
|
+
const accepts = (candidate) => valueNames.has(candidate) ||
|
|
118
|
+
(forwardsAttributes && isHtmlAttribute(candidate));
|
|
119
|
+
if (!accepts(prop)) {
|
|
120
|
+
// A React spelling of an attribute the story could give as HTML
|
|
121
|
+
// spells it, the form every pane takes.
|
|
122
|
+
const html = HTML_ATTRIBUTE_NAMES.get(prop);
|
|
123
|
+
errors.push(html !== undefined && accepts(html)
|
|
124
|
+
? `${name}: ${where} sets unknown prop '${prop}'; a story spells native attributes as HTML does: '${html}'`
|
|
125
|
+
: `${name}: ${where} sets unknown prop '${prop}'`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (prop === 'children' && component.defaultSlotScoped) {
|
|
129
|
+
errors.push(`${name}: ${where} sets children, which ${name}'s default slot cannot take: it exposes values, so its content is a function of them, not text`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (where.startsWith('scenario') && prop in fixed) {
|
|
133
|
+
errors.push(`${name}: ${where} sets '${prop}', which is fixed`);
|
|
134
|
+
}
|
|
135
|
+
// A scenario's undefined never reaches the panes, which keep the value
|
|
136
|
+
// the story starts the prop from; null is what unsets it. The scenario
|
|
137
|
+
// still renders, so this is a warning, an error under `strict`.
|
|
138
|
+
if (value === undefined &&
|
|
139
|
+
where.startsWith('scenario') &&
|
|
140
|
+
model.defaultProps[prop] !== undefined &&
|
|
141
|
+
JSON.stringify(component.props[prop]?.default) !==
|
|
142
|
+
JSON.stringify(model.defaultProps[prop])) {
|
|
143
|
+
warnings.push(`${name}: ${where} sets ${prop} to undefined, which leaves it at ${format(model.defaultProps[prop])}, the value the story starts it from: unset it with null`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const definition = component.props[prop];
|
|
147
|
+
if (!definition || value === undefined)
|
|
148
|
+
continue;
|
|
149
|
+
// A two-way prop given null is controlled at null, and its code binds
|
|
150
|
+
// state starting at null, which the component's types take only where
|
|
151
|
+
// the prop is declared with null.
|
|
152
|
+
if (value === null &&
|
|
153
|
+
(model.bindings ?? []).some((binding) => binding.prop === prop) &&
|
|
154
|
+
!unionMembers(definition.type).includes('null')) {
|
|
155
|
+
errors.push(`${name}: ${where} gives two-way prop ${prop} null, which its type '${definition.type}' does not take: declare it '${definition.type} | null', or leave ${prop} out`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
// The type as the control reads it: a named type expanded through
|
|
159
|
+
// the template's imports where it was.
|
|
160
|
+
const parsed = parseType(definition.resolvedType ?? definition.type);
|
|
161
|
+
// A prop with the color select takes one of the kit's color tokens;
|
|
162
|
+
// any other prop, color named or not, takes its declared union.
|
|
163
|
+
const isColor = model.propTypes.some((propType) => propType.name === prop && propType.control === 'color-select');
|
|
164
|
+
const allowed = isColor && hints.colorTokens
|
|
165
|
+
? hints.colorTokens.filter((token) => token != null)
|
|
166
|
+
: parsed.kind === 'enum'
|
|
167
|
+
? parsed.options
|
|
168
|
+
: undefined;
|
|
169
|
+
// A null unsets the prop (a two-way prop's is checked above).
|
|
170
|
+
if (allowed === undefined || value === null)
|
|
171
|
+
continue;
|
|
172
|
+
// Beside its literals, the type may take any string (`string & {}`)
|
|
173
|
+
// or a structured value (an object, an array), which the select
|
|
174
|
+
// cannot offer and a story file sets.
|
|
175
|
+
const open = !isColor && parsed.kind === 'enum' && parsed.open === true;
|
|
176
|
+
const structural = !isColor && parsed.kind === 'enum' && parsed.structural === true;
|
|
177
|
+
const takes = typeof value === 'string'
|
|
178
|
+
? open || allowed.includes(value)
|
|
179
|
+
: typeof value === 'boolean'
|
|
180
|
+
? allowed.includes(value)
|
|
181
|
+
: typeof value === 'object' && structural;
|
|
182
|
+
if (!takes) {
|
|
183
|
+
const options = allowed
|
|
184
|
+
.map((option) => typeof option === 'string' ? `'${option}'` : String(option))
|
|
185
|
+
.join(', ');
|
|
186
|
+
const besides = [
|
|
187
|
+
...(open ? ['any string'] : []),
|
|
188
|
+
...(structural ? ['a structured value'] : []),
|
|
189
|
+
];
|
|
190
|
+
errors.push(`${name}: ${where} sets ${prop} to ${format(value)}, not one of ${options}${besides.length ? ` or ${besides.join(' or ')}` : ''}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Named slots: a template's declared slot names, or for a driver read from
|
|
195
|
+
// code any prop it declares (in React a named or scoped slot is a prop).
|
|
196
|
+
const slotNames = new Set([
|
|
197
|
+
...component.slotNames,
|
|
198
|
+
...(component.memberNames ?? []),
|
|
199
|
+
]);
|
|
200
|
+
// The story's plan, and each scenario's own, which fills the same slots.
|
|
201
|
+
const plans = [
|
|
202
|
+
['', story.slots],
|
|
203
|
+
...(story.scenarios ?? []).map((scenario) => [
|
|
204
|
+
`scenario '${scenario.id}' `,
|
|
205
|
+
scenario.slots,
|
|
206
|
+
]),
|
|
207
|
+
];
|
|
208
|
+
// A slot whose content must take its scope takes a scoped fill alone: in
|
|
209
|
+
// React its prop is the render function, which plain content is not.
|
|
210
|
+
const scopeRequired = new Set(component.scopeRequiredSlots ?? []);
|
|
211
|
+
// The components the fills compose, at every depth, against what each
|
|
212
|
+
// declares.
|
|
213
|
+
const fillTrees = [];
|
|
214
|
+
for (const [where, plan] of plans) {
|
|
215
|
+
for (const slot of Object.keys(plan?.named ?? {})) {
|
|
216
|
+
if (!slotNames.has(slot))
|
|
217
|
+
errors.push(`${name}: ${where}slots fill named slot '${slot}', which ${name} lacks`);
|
|
218
|
+
}
|
|
219
|
+
const defaultFill = plan?.default;
|
|
220
|
+
const emptyFragment = defaultFill?.kind === 'fragment' && defaultFill.children.length === 0;
|
|
221
|
+
if (defaultFill && !emptyFragment && !component.hasDefaultSlot) {
|
|
222
|
+
errors.push(`${name}: ${where}slots fill the default slot, which ${name} lacks`);
|
|
223
|
+
}
|
|
224
|
+
const fills = [
|
|
225
|
+
['default', emptyFragment ? undefined : defaultFill],
|
|
226
|
+
...Object.entries(plan?.named ?? {}),
|
|
227
|
+
];
|
|
228
|
+
for (const [slot, fill] of fills) {
|
|
229
|
+
if (fill && fill.kind !== 'scoped' && scopeRequired.has(slot))
|
|
230
|
+
errors.push(`${name}: ${where}slots fill ${slot === 'default' ? 'the default slot' : `named slot '${slot}'`} with plain content, which it cannot take: its content must take the values it exposes, so fill it with a scoped fill`);
|
|
231
|
+
}
|
|
232
|
+
fillTrees.push([`${where}slots.default`, emptyFragment ? undefined : defaultFill], ...Object.entries(plan?.named ?? {}).map(([slot, fill]) => [`${where}slots.named.${slot}`, fill]));
|
|
233
|
+
}
|
|
234
|
+
for (const referenced of model.slotComponents) {
|
|
235
|
+
if (!knownComponents.has(referenced)) {
|
|
236
|
+
errors.push(`${name}: slot content references '${referenced}', which no source declares`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
fillTrees.push(['around.before', story.around?.before], ['around.after', story.around?.after]);
|
|
240
|
+
for (const [where, fill] of fillTrees)
|
|
241
|
+
checkFillTree(name, where, fill, knownComponents, errors);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Checks the option values a component's scenarios set against the kit's
|
|
245
|
+
* options: each must name one of them, hold a value it accepts, and not be
|
|
246
|
+
* one the story file makes unavailable for the component.
|
|
247
|
+
*/
|
|
248
|
+
function checkScenarioOptions({ component, story }, previewOptions, errors) {
|
|
249
|
+
const options = Array.isArray(previewOptions)
|
|
250
|
+
? previewOptions
|
|
251
|
+
: [];
|
|
252
|
+
for (const scenario of story?.scenarios ?? []) {
|
|
253
|
+
for (const [id, value] of Object.entries(scenario.options ?? {})) {
|
|
254
|
+
const where = `${component.name}: scenario '${scenario.id}'`;
|
|
255
|
+
const option = options.find((candidate) => candidate?.id === id);
|
|
256
|
+
if (!option) {
|
|
257
|
+
errors.push(`${where} sets unknown option '${id}'`);
|
|
258
|
+
}
|
|
259
|
+
else if (!acceptsOptionValue(option, value)) {
|
|
260
|
+
errors.push(`${where} sets option '${id}' to ${JSON.stringify(value)}, which it cannot hold`);
|
|
261
|
+
}
|
|
262
|
+
else if (story?.options?.[id] === false) {
|
|
263
|
+
errors.push(`${where} sets option '${id}', which the story file makes unavailable`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Whether a package's `exports` exports a subpath, as Node resolves it: an
|
|
270
|
+
* entry for the subpath itself, or else the subpath pattern (`./*`) with the
|
|
271
|
+
* longest matching prefix. An entry whose value is `null` excludes the
|
|
272
|
+
* subpath.
|
|
273
|
+
*/
|
|
274
|
+
function exportsSubpath(exports, subpath) {
|
|
275
|
+
if (exports === null || typeof exports !== 'object' || Array.isArray(exports))
|
|
276
|
+
return false;
|
|
277
|
+
const entries = exports;
|
|
278
|
+
if (subpath in entries)
|
|
279
|
+
return entries[subpath] !== null;
|
|
280
|
+
let best;
|
|
281
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
282
|
+
const star = key.indexOf('*');
|
|
283
|
+
if (star === -1 || key.indexOf('*', star + 1) !== -1)
|
|
284
|
+
continue;
|
|
285
|
+
const prefix = key.slice(0, star);
|
|
286
|
+
const suffix = key.slice(star + 1);
|
|
287
|
+
if (subpath.length >= prefix.length + suffix.length &&
|
|
288
|
+
subpath.startsWith(prefix) &&
|
|
289
|
+
subpath.endsWith(suffix) &&
|
|
290
|
+
(best === undefined || prefix.length > best.prefix.length))
|
|
291
|
+
best = { prefix, value };
|
|
292
|
+
}
|
|
293
|
+
return best !== undefined && best.value !== null;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Whether a package's `exports` exports a shared module's subpath, or paths
|
|
297
|
+
* below it, which the kit's built files import as `<specifier>/<path>`: an
|
|
298
|
+
* entry for the subpath (see {@link exportsSubpath}) or any entry below it,
|
|
299
|
+
* such as the pattern `./scripts/*` for `./scripts`, which the kit's
|
|
300
|
+
* files import paths below the specifier by. The package's own name,
|
|
301
|
+
* `.`, is also exported by a string or array `exports`, or an object of
|
|
302
|
+
* conditions alone (`{ "import": ..., "types": ... }`).
|
|
303
|
+
*/
|
|
304
|
+
function exportsShared(exports, subpath) {
|
|
305
|
+
if (subpath === '.') {
|
|
306
|
+
if (typeof exports === 'string' || Array.isArray(exports))
|
|
307
|
+
return true;
|
|
308
|
+
if (exports !== null &&
|
|
309
|
+
typeof exports === 'object' &&
|
|
310
|
+
Object.keys(exports).length > 0 &&
|
|
311
|
+
!Object.keys(exports).some((key) => key.startsWith('.')))
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
if (exportsSubpath(exports, subpath))
|
|
315
|
+
return true;
|
|
316
|
+
if (subpath === '.' ||
|
|
317
|
+
exports === null ||
|
|
318
|
+
typeof exports !== 'object' ||
|
|
319
|
+
Array.isArray(exports))
|
|
320
|
+
return false;
|
|
321
|
+
return Object.entries(exports).some(([key, value]) => key.startsWith(`${subpath}/`) && value !== null);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The entry a package's `exports` could give a shared module's source: the
|
|
325
|
+
* file itself, or a directory's `index.ts` or `index.js` when it has one,
|
|
326
|
+
* relative to the package's directory. `undefined` when the source is a
|
|
327
|
+
* directory with neither.
|
|
328
|
+
*/
|
|
329
|
+
function sharedEntry(packageDirectory, kitRoot, source) {
|
|
330
|
+
const absolute = resolve(kitRoot, source);
|
|
331
|
+
const path = `./${relative(packageDirectory, absolute).split(sep).join('/')}`;
|
|
332
|
+
if (!statSync(absolute, { throwIfNoEntry: false })?.isDirectory())
|
|
333
|
+
return path;
|
|
334
|
+
const index = ['index.ts', 'index.js'].find((file) => existsSync(resolve(absolute, file)));
|
|
335
|
+
return index === undefined ? undefined : `${path}/${index}`;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* A package's `package.json`, read for its name and `exports`; `undefined`
|
|
339
|
+
* when it has none, and an error when it is not JSON.
|
|
340
|
+
*/
|
|
341
|
+
function readCheckedPackage(directory, label, errors) {
|
|
342
|
+
const packagePath = resolve(directory, 'package.json');
|
|
343
|
+
if (!existsSync(packagePath))
|
|
344
|
+
return undefined;
|
|
345
|
+
let manifest;
|
|
346
|
+
try {
|
|
347
|
+
manifest = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
errors.push(`${packagePath}: cannot be read as JSON (${error.message})`);
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
const { name, exports } = manifest ?? {};
|
|
354
|
+
if (typeof name !== 'string')
|
|
355
|
+
return undefined;
|
|
356
|
+
return { label, name, directory, packagePath, exports };
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Checks that the kit's package exports each path the preview imports it
|
|
360
|
+
* by: a source whose `moduleBase` is the kit's own package name imports
|
|
361
|
+
* `<name>/<target>`, which resolves only through an `exports` entry for
|
|
362
|
+
* `./<target>` (or a subpath pattern covering it), and the kit's built
|
|
363
|
+
* files import each shared module by its `add.shared` `specifier`, which
|
|
364
|
+
* resolves the same way when it is a path of the kit's own package. A
|
|
365
|
+
* module the kit aliases is resolved by the alias instead.
|
|
366
|
+
*
|
|
367
|
+
* A kit built into one package per target (its configuration's
|
|
368
|
+
* `packages`) is checked as its packages: a shared module's specifier
|
|
369
|
+
* against the exports of the package it names (the kit root's or the
|
|
370
|
+
* styles package's), the kit's own source against the target packages'
|
|
371
|
+
* names, and each package for resolving from the kit root.
|
|
372
|
+
*/
|
|
373
|
+
function checkTargetExports(context, errors) {
|
|
374
|
+
const aliases = context.configuration.aliases ?? {};
|
|
375
|
+
const kit = readCheckedPackage(context.kitRoot, "the kit's exports", errors);
|
|
376
|
+
const layout = context.kitPackages;
|
|
377
|
+
const styles = layout?.find(({ role }) => role === 'styles');
|
|
378
|
+
const styled = styles === undefined
|
|
379
|
+
? undefined
|
|
380
|
+
: readCheckedPackage(styles.directoryAbsolute, "the styles package's exports", errors);
|
|
381
|
+
const exporting = [kit, styled].filter((checked) => checked !== undefined && !(checked.name in aliases));
|
|
382
|
+
for (const { source, specifier } of context.sharedSpecifiers) {
|
|
383
|
+
const holder = exporting
|
|
384
|
+
.filter(({ name }) => specifier === name || specifier.startsWith(`${name}/`))
|
|
385
|
+
.sort((a, b) => b.name.length - a.name.length)[0];
|
|
386
|
+
if (holder === undefined)
|
|
387
|
+
continue;
|
|
388
|
+
const subpath = `.${specifier.slice(holder.name.length)}`;
|
|
389
|
+
if (specifier in aliases || exportsShared(holder.exports, subpath))
|
|
390
|
+
continue;
|
|
391
|
+
const entry = sharedEntry(holder.directory, context.kitRoot, source);
|
|
392
|
+
errors.push(`${holder.packagePath}: add.shared gives '${source}' the specifier '${specifier}', but ${holder.label} has no '${subpath}' entry, so the preview and the kit's consumers cannot import it; ${entry === undefined
|
|
393
|
+
? `add one naming the module '${source}' exports from`
|
|
394
|
+
: `add one, such as "${subpath}": "${entry}"`}`);
|
|
395
|
+
}
|
|
396
|
+
if (layout !== undefined) {
|
|
397
|
+
checkLayoutSources(context, kit?.name, errors);
|
|
398
|
+
checkLayoutResolves(context, layout, errors);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (kit === undefined || kit.name in aliases)
|
|
402
|
+
return;
|
|
403
|
+
const { name, exports, packagePath } = kit;
|
|
404
|
+
if (!context.sources.some((source) => source.moduleBase === name))
|
|
405
|
+
return;
|
|
406
|
+
for (const target of context.targets) {
|
|
407
|
+
const specifier = `${name}/${target}`;
|
|
408
|
+
if (specifier in aliases || exportsSubpath(exports, `./${target}`))
|
|
409
|
+
continue;
|
|
410
|
+
errors.push(`${packagePath}: the kit builds the ${target} target, but its package.json has no exports entry for './${target}', so the preview cannot import '${specifier}'; add one, such as "./${target}": "./dist/${target}/index.ts"`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* The `moduleBase` with `{target}` whose module for each target is that
|
|
415
|
+
* target's package name, when one covers them all.
|
|
416
|
+
*/
|
|
417
|
+
function targetPattern(packages) {
|
|
418
|
+
const [first] = packages;
|
|
419
|
+
if (first === undefined)
|
|
420
|
+
return undefined;
|
|
421
|
+
for (let index = first.name.indexOf(first.role); index !== -1; index = first.name.indexOf(first.role, index + 1)) {
|
|
422
|
+
const pattern = `${first.name.slice(0, index)}${TARGET_TOKEN}${first.name.slice(index + first.role.length)}`;
|
|
423
|
+
if (packages.every(({ role, name }) => moduleOfSource(pattern, role) === name))
|
|
424
|
+
return pattern;
|
|
425
|
+
}
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Checks, for a kit built into one package per target, that the
|
|
430
|
+
* kit's own source imports each target from its package: a source naming the
|
|
431
|
+
* kit (its package name or kit name, without `{target}`) or a `{target}`
|
|
432
|
+
* pattern that yields some target packages' names but not the rest is
|
|
433
|
+
* reported, with the pattern that covers the configured names.
|
|
434
|
+
*/
|
|
435
|
+
function checkLayoutSources(context, kitPackageName, errors) {
|
|
436
|
+
const aliases = context.configuration.aliases ?? {};
|
|
437
|
+
const targetPackages = (context.kitPackages ?? []).filter(({ role }) => role !== 'styles');
|
|
438
|
+
if (targetPackages.length === 0)
|
|
439
|
+
return;
|
|
440
|
+
const pattern = targetPattern(targetPackages);
|
|
441
|
+
const remedy = pattern === undefined
|
|
442
|
+
? `name the per-target packages so one pattern with '${TARGET_TOKEN}' covers them all (such as '${context.kitName}-${TARGET_TOKEN}'), and set moduleBase to it`
|
|
443
|
+
: `set moduleBase to '${pattern}'`;
|
|
444
|
+
for (const source of context.sources) {
|
|
445
|
+
const { moduleBase } = source;
|
|
446
|
+
const namesKit = !moduleBase.includes(TARGET_TOKEN) &&
|
|
447
|
+
(moduleBase === kitPackageName || moduleBase === context.kitName);
|
|
448
|
+
const matches = targetPackages.filter(({ role, name }) => moduleOfSource(moduleBase, role) === name);
|
|
449
|
+
if (!namesKit && matches.length === 0)
|
|
450
|
+
continue;
|
|
451
|
+
for (const { role, name } of targetPackages) {
|
|
452
|
+
const imported = moduleOfSource(moduleBase, role);
|
|
453
|
+
if (imported === name || imported in aliases)
|
|
454
|
+
continue;
|
|
455
|
+
errors.push(`source '${source.id}': moduleBase '${moduleBase}' imports '${imported}', but the ${role} target is built into '${name}'; ${remedy}`);
|
|
456
|
+
// One message per source says what to change.
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Checks that each package of a kit built into one package per target
|
|
463
|
+
* resolves from the kit root, as the preview imports it by name.
|
|
464
|
+
*/
|
|
465
|
+
function checkLayoutResolves(context, layout, errors) {
|
|
466
|
+
const aliases = context.configuration.aliases ?? {};
|
|
467
|
+
for (const { role, name } of layout) {
|
|
468
|
+
if (name in aliases || resolvesFrom(name, context.kitRoot))
|
|
469
|
+
continue;
|
|
470
|
+
errors.push(`packages.${role}: '${name}' does not resolve from the kit root, so the preview cannot import it; add it to the kit's workspace or as a dev dependency of the kit root`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/** `diagnoseKit` from a module's exports, ES or CommonJS, when it has one. */
|
|
474
|
+
function diagnoseKitOf(module) {
|
|
475
|
+
const exports = module;
|
|
476
|
+
const found = exports.diagnoseKit ?? exports.default?.diagnoseKit;
|
|
477
|
+
return typeof found === 'function' ? found : undefined;
|
|
478
|
+
}
|
|
479
|
+
/** Whether a report is the shape `diagnoseKit` returns: lists of lines. */
|
|
480
|
+
function isKitDiagnostics(report) {
|
|
481
|
+
const lines = (value) => Array.isArray(value) && value.every((line) => typeof line === 'string');
|
|
482
|
+
return (typeof report === 'object' &&
|
|
483
|
+
report !== null &&
|
|
484
|
+
lines(report.errors) &&
|
|
485
|
+
lines(report.warnings));
|
|
486
|
+
}
|
|
487
|
+
/** Whether an error says a module is not there to load. */
|
|
488
|
+
function isMissingModule(error) {
|
|
489
|
+
const code = error?.code;
|
|
490
|
+
return (code === 'MODULE_NOT_FOUND' || code === 'ERR_PACKAGE_PATH_NOT_EXPORTED');
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* What `scaffold-ui-kit build` reports for the kit's components, by the
|
|
494
|
+
* scaffolder the kit builds with: the kit's own installed copy, else the
|
|
495
|
+
* preview's. A kit copy that cannot be loaded, has no `diagnoseKit` or
|
|
496
|
+
* returns something other than its report is set aside for the preview's,
|
|
497
|
+
* with a warning saying why; a kit with none installed is not warned of. A
|
|
498
|
+
* kit with no `src/components/`, whose components are all manual or come
|
|
499
|
+
* from another directory, has nothing the build renders, and gets nothing
|
|
500
|
+
* here.
|
|
501
|
+
*/
|
|
502
|
+
async function buildDiagnostics(kitRoot, configuration) {
|
|
503
|
+
if (!existsSync(join(kitRoot, 'src', 'components')))
|
|
504
|
+
return { errors: [], warnings: [] };
|
|
505
|
+
const fallback = async (reason) => {
|
|
506
|
+
const own = diagnoseKitOf(await import('scaffold-ui-kit'));
|
|
507
|
+
const report = own
|
|
508
|
+
? await own(kitRoot, { configuration })
|
|
509
|
+
: { errors: [], warnings: [] };
|
|
510
|
+
return reason === undefined
|
|
511
|
+
? report
|
|
512
|
+
: {
|
|
513
|
+
errors: report.errors,
|
|
514
|
+
warnings: [
|
|
515
|
+
`the kit's scaffold-ui-kit ${reason}, so the build's diagnostics come from the preview's own copy`,
|
|
516
|
+
...report.warnings,
|
|
517
|
+
],
|
|
518
|
+
};
|
|
519
|
+
};
|
|
520
|
+
let entry;
|
|
521
|
+
try {
|
|
522
|
+
entry = createRequire(join(kitRoot, 'package.json')).resolve('scaffold-ui-kit');
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
if (isMissingModule(error))
|
|
526
|
+
return fallback();
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
let module;
|
|
530
|
+
try {
|
|
531
|
+
module = await import(pathToFileURL(entry).href);
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
return fallback(`could not be loaded (${error instanceof Error ? error.message : String(error)})`);
|
|
535
|
+
}
|
|
536
|
+
const diagnoseKit = diagnoseKitOf(module);
|
|
537
|
+
if (!diagnoseKit)
|
|
538
|
+
return fallback('exports no diagnoseKit; update scaffold-ui-kit in the kit');
|
|
539
|
+
const report = await diagnoseKit(kitRoot, { configuration });
|
|
540
|
+
if (!isKitDiagnostics(report))
|
|
541
|
+
return fallback("returned a report that isn't lists of errors and warnings");
|
|
542
|
+
return report;
|
|
543
|
+
}
|
|
544
|
+
/** The component a tag names, or undefined for an HTML element: the part
|
|
545
|
+
* before any `.`, when it starts with a capital. A dynamic tag names its
|
|
546
|
+
* `default`. */
|
|
547
|
+
function componentOfTag(tag) {
|
|
548
|
+
const name = typeof tag === 'string'
|
|
549
|
+
? tag
|
|
550
|
+
: tag && typeof tag === 'object'
|
|
551
|
+
? tag.default
|
|
552
|
+
: undefined;
|
|
553
|
+
if (typeof name !== 'string')
|
|
554
|
+
return undefined;
|
|
555
|
+
const head = name.split('.')[0];
|
|
556
|
+
return /^[A-Z]/.test(head) ? head : undefined;
|
|
557
|
+
}
|
|
558
|
+
/** Every component an element or fragment in a template tree renders, through
|
|
559
|
+
* its tag or a target's `tag` override. */
|
|
560
|
+
function collectRendered(node, into) {
|
|
561
|
+
if (!node || typeof node !== 'object')
|
|
562
|
+
return;
|
|
563
|
+
if (Array.isArray(node)) {
|
|
564
|
+
for (const child of node)
|
|
565
|
+
collectRendered(child, into);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const record = node;
|
|
569
|
+
if (record.type === 'element' || record.type === 'fragment') {
|
|
570
|
+
const own = componentOfTag(record.tag);
|
|
571
|
+
if (own)
|
|
572
|
+
into.add(own);
|
|
573
|
+
const extensions = record.extensions;
|
|
574
|
+
if (extensions && typeof extensions === 'object') {
|
|
575
|
+
for (const override of Object.values(extensions)) {
|
|
576
|
+
if (override && typeof override === 'object') {
|
|
577
|
+
const named = componentOfTag(override.tag);
|
|
578
|
+
if (named)
|
|
579
|
+
into.add(named);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
for (const value of Object.values(record))
|
|
585
|
+
collectRendered(value, into);
|
|
586
|
+
}
|
|
587
|
+
/** Each component's renderers: the other templates whose trees render it,
|
|
588
|
+
* omitted ones included. A story file's slot plan is not a renderer. */
|
|
589
|
+
function templateRenderers(components) {
|
|
590
|
+
const renderers = new Map();
|
|
591
|
+
for (const { component } of components) {
|
|
592
|
+
if (!component.template)
|
|
593
|
+
continue;
|
|
594
|
+
const rendered = new Set();
|
|
595
|
+
collectRendered(component.template, rendered);
|
|
596
|
+
rendered.delete(component.name);
|
|
597
|
+
for (const name of rendered) {
|
|
598
|
+
const set = renderers.get(name) ?? new Set();
|
|
599
|
+
set.add(component.name);
|
|
600
|
+
renderers.set(name, set);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return renderers;
|
|
604
|
+
}
|
|
605
|
+
/** The labels of the stories a reader browses, each as `<source> <label>`: a
|
|
606
|
+
* standalone component neither omitted nor unlisted, whose story file names its
|
|
607
|
+
* story. A label stands in for a component of its own source only. */
|
|
608
|
+
function labelsOfStories(components, omitPattern) {
|
|
609
|
+
const labels = new Set();
|
|
610
|
+
for (const { component, story, source } of components) {
|
|
611
|
+
const label = story?.label;
|
|
612
|
+
if (typeof label !== 'string')
|
|
613
|
+
continue;
|
|
614
|
+
const omit = (story?.omit ?? false) ||
|
|
615
|
+
isStructurallyOmitted(component.name, omitPattern);
|
|
616
|
+
const standalone = (story?.standalone ?? true) && (component.standalone ?? true);
|
|
617
|
+
const unlisted = (story?.unlisted ?? false) || (component.unlisted ?? false);
|
|
618
|
+
if (!omit && standalone && !unlisted)
|
|
619
|
+
labels.add(`${source.id} ${label}`);
|
|
620
|
+
}
|
|
621
|
+
return labels;
|
|
622
|
+
}
|
|
623
|
+
/** Whether a component's template is a compound's root, listing parts. */
|
|
624
|
+
function listsParts(template) {
|
|
625
|
+
if (!template || typeof template !== 'object' || Array.isArray(template))
|
|
626
|
+
return false;
|
|
627
|
+
const parts = template.parts;
|
|
628
|
+
return Array.isArray(parts) && parts.length > 0;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Checks a kit's preview story files and manual specs against its components.
|
|
632
|
+
*
|
|
633
|
+
* @param kitRoot - The kit root (where its package.json lives)
|
|
634
|
+
* @param options - `strict` turns coverage warnings into errors; `configuration`
|
|
635
|
+
* names the kit configuration file, as `--configuration` does
|
|
636
|
+
* @returns The errors, warnings and hidden props, each a line naming its component
|
|
637
|
+
* @example
|
|
638
|
+
* ```ts
|
|
639
|
+
* it('keeps the preview in step with the components', async () => {
|
|
640
|
+
* const report = await checkPreview(process.cwd());
|
|
641
|
+
* expect(report.errors).toEqual([]);
|
|
642
|
+
* });
|
|
643
|
+
* ```
|
|
644
|
+
*/
|
|
645
|
+
export async function checkPreview(kitRoot, options = {}) {
|
|
646
|
+
const context = loadPreviewContext(kitRoot, {
|
|
647
|
+
configuration: options.configuration,
|
|
648
|
+
workingDirectory: options.workingDirectory,
|
|
649
|
+
});
|
|
650
|
+
const { components, parts } = await loadKit(context);
|
|
651
|
+
const kitConfiguration = context.configuration;
|
|
652
|
+
const hints = kitConfiguration.controlHints ?? {};
|
|
653
|
+
const errors = [];
|
|
654
|
+
const warnings = [];
|
|
655
|
+
const hidden = [];
|
|
656
|
+
const ungenerated = [];
|
|
657
|
+
const knownComponents = new Map(components.map((loaded) => [loaded.component.name, loaded.component]));
|
|
658
|
+
const renderers = templateRenderers(components);
|
|
659
|
+
const storyLabels = labelsOfStories(components, context.omitPattern);
|
|
660
|
+
errors.push(...optionProblems(kitConfiguration.options));
|
|
661
|
+
errors.push(...kitPaneWidths(context).errors);
|
|
662
|
+
const shellProblem = missingShellPeersProblem(missingShellPeers(context));
|
|
663
|
+
if (shellProblem !== undefined)
|
|
664
|
+
errors.push(shellProblem);
|
|
665
|
+
// The shell's page, loaded as `dev` and `build` load it.
|
|
666
|
+
try {
|
|
667
|
+
await loadShellPage(context, { hostPage: HOST_PAGE, serving: false });
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
errors.push(error.message.replace(/^\[preview\] /, ''));
|
|
671
|
+
}
|
|
672
|
+
checkTargetExports(context, errors);
|
|
673
|
+
// What the kit's own build reports for its components, as it reports it:
|
|
674
|
+
// a story of a component the build fails has nothing to show.
|
|
675
|
+
const build = await buildDiagnostics(resolve(kitRoot), context.configurationPath);
|
|
676
|
+
errors.push(...build.errors);
|
|
677
|
+
// The browseable stories, which the landing entry may name.
|
|
678
|
+
const stories = [];
|
|
679
|
+
for (const loaded of components) {
|
|
680
|
+
const { component, story } = loaded;
|
|
681
|
+
let model;
|
|
682
|
+
try {
|
|
683
|
+
model = resolveStory(component, story, hints, kitConfiguration.labels, context.omitPattern, context.targets);
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
errors.push(error.message);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
// `unlisted` hides a story; an omitted component or one that is not
|
|
690
|
+
// standalone has none.
|
|
691
|
+
if ((story?.unlisted ?? false) || (component.unlisted ?? false)) {
|
|
692
|
+
if (model.omit)
|
|
693
|
+
errors.push(`${component.name}: unlisted has no story to hide, as the component is omitted`);
|
|
694
|
+
else if (!model.standalone)
|
|
695
|
+
errors.push(`${component.name}: unlisted has no story to hide, as the component is standalone: false`);
|
|
696
|
+
}
|
|
697
|
+
// A compound's root names its parts in its template, or in the kit
|
|
698
|
+
// configuration when it has none; a story file's
|
|
699
|
+
// `standalone: false` alone still takes a component out of the navigation,
|
|
700
|
+
// but no compound copies it whole. One that is no one compound's part
|
|
701
|
+
// needs no listing: a block two
|
|
702
|
+
// templates render, a component a labeled story stands in for, or a
|
|
703
|
+
// root with parts of its own.
|
|
704
|
+
if (story?.standalone === false &&
|
|
705
|
+
!parts.has(component.name) &&
|
|
706
|
+
(renderers.get(component.name)?.size ?? 0) < 2 &&
|
|
707
|
+
!storyLabels.has(`${loaded.source.id} ${component.name}`) &&
|
|
708
|
+
!listsParts(component.template) &&
|
|
709
|
+
(context.parts[component.name]?.length ?? 0) === 0)
|
|
710
|
+
warnings.push(`${component.name}: its story file sets standalone: false, but no compound lists it in parts, fewer than two templates render it, no story in its source is labeled '${component.name}' and it lists no parts of its own; list it in its root's parts`);
|
|
711
|
+
if (!model.omit && model.standalone)
|
|
712
|
+
stories.push(model);
|
|
713
|
+
checkComponent(loaded, model, knownComponents, hints, errors, warnings);
|
|
714
|
+
checkScenarioOptions(loaded, kitConfiguration.options, errors);
|
|
715
|
+
// Omitted components get no story, so their coverage is moot. A prop the
|
|
716
|
+
// story file's `generate` leaves out is out of coverage by that choice, and is
|
|
717
|
+
// listed as ungenerated instead.
|
|
718
|
+
if (!model.omit && model.standalone) {
|
|
719
|
+
const generate = story?.generate;
|
|
720
|
+
const ungeneratedProps = new Set();
|
|
721
|
+
for (const { prop, value } of scenarioValues(model.propTypes, model.defaultProps)) {
|
|
722
|
+
if (!generatesProp(generate, prop)) {
|
|
723
|
+
ungeneratedProps.add(prop);
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
if (!model.scenarios.some((scenario) => scenario.props?.[prop] === value)) {
|
|
727
|
+
const id = generatedScenarioId(prop, value);
|
|
728
|
+
const replacing = story?.scenarios?.some((scenario) => scenario.id === id);
|
|
729
|
+
warnings.push(`${component.name}: no scenario sets ${prop} to ${format(value)}` +
|
|
730
|
+
(replacing
|
|
731
|
+
? ` (scenario '${id}' replaces the generated one and does not set it)`
|
|
732
|
+
: ''));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
for (const prop of ungeneratedProps)
|
|
736
|
+
ungenerated.push(`${component.name}: ${prop}`);
|
|
737
|
+
}
|
|
738
|
+
for (const propType of model.propTypes) {
|
|
739
|
+
const excluded = story?.controls?.[propType.name] === false ||
|
|
740
|
+
propType.name in (story?.fixed ?? {});
|
|
741
|
+
if (!propType.include && !excluded) {
|
|
742
|
+
hidden.push(`${component.name}: ${propType.name} (${component.props[propType.name].type})`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
// A story file that matches no component in its source is never read. A
|
|
747
|
+
// stories directory the kit has not made yet holds none.
|
|
748
|
+
for (const source of context.sources) {
|
|
749
|
+
if (!source.storiesDirectoryAbsolute ||
|
|
750
|
+
!existsSync(source.storiesDirectoryAbsolute))
|
|
751
|
+
continue;
|
|
752
|
+
const componentNames = new Set(components
|
|
753
|
+
.filter((loaded) => loaded.source.id === source.id)
|
|
754
|
+
.map((loaded) => loaded.component.name));
|
|
755
|
+
for (const file of readdirSync(source.storiesDirectoryAbsolute)) {
|
|
756
|
+
if (!file.endsWith(STORY_FILE_SUFFIX))
|
|
757
|
+
continue;
|
|
758
|
+
const name = file.slice(0, -STORY_FILE_SUFFIX.length);
|
|
759
|
+
if (!componentNames.has(name)) {
|
|
760
|
+
errors.push(`${name}: ${resolve(source.storiesDirectoryAbsolute, file)} matches no component in source '${source.id}'; name it after a template, or list a hand-written component in the source's manualModule`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
errors.push(...landingProblems(context.configuration.landing, stories));
|
|
765
|
+
// `strict` makes the coverage gaps errors; what reading the kit
|
|
766
|
+
// configuration warns of, such as a file name it still accepts, and the
|
|
767
|
+
// build's warnings stay warnings.
|
|
768
|
+
return options.strict
|
|
769
|
+
? {
|
|
770
|
+
errors: [...errors, ...warnings],
|
|
771
|
+
warnings: [...context.warnings, ...build.warnings],
|
|
772
|
+
hidden,
|
|
773
|
+
ungenerated,
|
|
774
|
+
}
|
|
775
|
+
: {
|
|
776
|
+
errors,
|
|
777
|
+
warnings: [...context.warnings, ...build.warnings, ...warnings],
|
|
778
|
+
hidden,
|
|
779
|
+
ungenerated,
|
|
780
|
+
};
|
|
781
|
+
}
|