@fluentcommerce/ai-skills 0.2.0 → 0.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.
Potentially problematic release.
This version of @fluentcommerce/ai-skills might be problematic. Click here for more details.
- package/README.md +866 -616
- package/bin/cli.mjs +2112 -1973
- package/content/cli/agents/fluent-cli/agent.json +149 -149
- package/content/cli/agents/fluent-cli.md +132 -132
- package/content/cli/skills/fluent-bootstrap/SKILL.md +214 -190
- package/content/cli/skills/fluent-cli-index/SKILL.md +1 -1
- package/content/cli/skills/fluent-cli-mcp-cicd/SKILL.md +117 -1
- package/content/cli/skills/fluent-cli-reference/SKILL.md +1040 -623
- package/content/cli/skills/fluent-cli-retailer/SKILL.md +27 -2
- package/content/cli/skills/fluent-cli-settings/SKILL.md +21 -1
- package/content/cli/skills/fluent-connect/SKILL.md +937 -886
- package/content/cli/skills/fluent-module-deploy/SKILL.md +181 -17
- package/content/cli/skills/fluent-profile/SKILL.md +73 -0
- package/content/cli/skills/fluent-workflow/SKILL.md +360 -310
- package/content/dev/agents/fluent-backend-dev/AGENT.md +58 -0
- package/content/dev/agents/fluent-backend-dev/agent.json +69 -0
- package/content/dev/agents/fluent-backend-dev.md +287 -0
- package/content/dev/agents/fluent-dev/AGENT.md +98 -76
- package/content/dev/agents/fluent-dev/agent.json +24 -2
- package/content/dev/agents/fluent-dev.md +194 -524
- package/content/dev/agents/fluent-frontend-dev/AGENT.md +63 -0
- package/content/dev/agents/fluent-frontend-dev/agent.json +52 -0
- package/content/dev/agents/fluent-frontend-dev.md +323 -0
- package/content/dev/skills/fluent-archive/SKILL.md +234 -0
- package/content/dev/skills/fluent-build/SKILL.md +312 -170
- package/content/dev/skills/fluent-connection-analysis/SKILL.md +422 -386
- package/content/dev/skills/fluent-custom-code/SKILL.md +15 -9
- package/content/dev/skills/fluent-data-module-scaffold/SKILL.md +731 -0
- package/content/dev/skills/fluent-e2e-test/SKILL.md +501 -394
- package/content/dev/skills/fluent-event-api/SKILL.md +962 -945
- package/content/dev/skills/fluent-feature-explain/SKILL.md +680 -603
- package/content/dev/skills/fluent-feature-plan/PLAN_TEMPLATE.md +40 -11
- package/content/dev/skills/fluent-feature-plan/SKILL.md +478 -221
- package/content/dev/skills/fluent-feature-status/SKILL.md +335 -0
- package/content/dev/skills/fluent-feedback/SKILL.md +221 -0
- package/content/dev/skills/fluent-implementation-map/SKILL.md +644 -0
- package/content/dev/skills/fluent-job-batch/SKILL.md +10 -0
- package/content/dev/skills/fluent-module-scaffold/SKILL.md +134 -3
- package/content/dev/skills/fluent-module-validate/SKILL.md +778 -775
- package/content/dev/skills/fluent-mystique-analyze/SKILL.md +817 -0
- package/content/dev/skills/fluent-mystique-builder/COMPONENT_TEMPLATE.md +81 -0
- package/content/dev/skills/fluent-mystique-builder/README.md +63 -0
- package/content/dev/skills/fluent-mystique-builder/SKILL.md +1294 -0
- package/content/dev/skills/fluent-mystique-builder/components/INDEX.md +92 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.accordion.md +48 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.action.field.fulfilmentpack.md +20 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.action.field.multiparcel.md +21 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.action.field.returnitems.md +21 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.action.field.wavepick.md +21 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.action.inline.md +24 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.activity.entity.md +25 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.analytics.viz.md +20 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.attribute.column.md +111 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.attribute.json.md +20 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.attribute.jsoneditor.md +54 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.attribute.locationId.md +51 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.attribute.retailerId.md +52 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.button.bar.md +57 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.button.print.download.md +53 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.button.print.inline.compatibility.md +60 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.button.print.inline.md +53 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.button.print.md +24 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.button.print.pick.md +61 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.buttons.add.reject.md +20 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.card.attribute.md +73 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.card.attributes.grid.md +40 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.card.image.md +37 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.card.map.point.md +24 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.card.multi.md +79 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.card.product.md +27 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.chart.area.md +34 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.chart.area.wrapper.feed.md +98 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.chart.bar.md +52 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.chart.bar.wrapper.source.md +104 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.chart.gauge.md +28 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.chart.gauge.wrapper.threshold.md +118 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.chart.line.md +32 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.conditional.md +62 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.dashboard.threshold.md +65 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.daterange.wrapper.forwarder.md +56 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.drawer.button.md +21 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.event.detail.md +20 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.events.search.md +21 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.field.daterange.md +83 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.field.filterComplex.md +106 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.field.intrange.md +82 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.field.multistring.md +50 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.filterPanel.md +53 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.json.editor.md +22 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.json.viewer.md +21 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.list.customAction.md +79 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.list.md +116 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.list.wrapper.bppmetrics.md +69 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.list.wrapper.feed.md +65 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.list.wrapper.source.md +64 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.modal.button.addItem.md +60 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.modal.button.md +21 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.mutation.inline.md +88 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.mystique.collapsible.attributes.md +83 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.mystique.collapsible.text.md +33 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.mystique.link.md +30 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.order.itemDetails.md +20 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.order.shipmentDetails.md +20 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.page.filter.select.md +87 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.page.md +64 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.page.refresh.md +48 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.page.section.column.md +71 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.page.section.header.md +61 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.page.section.md +59 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.page.wizard.md +45 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.page.wizard.summary.md +56 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.progress.circular.md +20 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.provider.graphql.md +71 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.quantity.list.md +87 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.repeater.md +56 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.reports.ipuipc.md +54 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.return.rowExpansion.md +19 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.scanner.barcode.md +21 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.scanner.barcodeFilter.md +72 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.scanner.camera.md +20 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.settingForm.md +64 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.sourcing.profile.drawer.button.md +19 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.sourcing.profile.modal.button.md +64 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.sourcing.strategy.modal.button.md +20 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.stepper.md +20 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.tab.content.md +56 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.tabs.card.md +64 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.tabs.md +69 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.tile.metric.md +73 -0
- package/content/dev/skills/fluent-mystique-builder/components/fc.workflow.provider.md +77 -0
- package/content/dev/skills/fluent-mystique-builder/validate-docs.ps1 +260 -0
- package/content/dev/skills/fluent-mystique-scaffold/SKILL.md +1830 -0
- package/content/dev/skills/fluent-mystique-validate/SKILL.md +646 -0
- package/content/dev/skills/fluent-pre-deploy-check/SKILL.md +1144 -1090
- package/content/dev/skills/fluent-retailer-config/SKILL.md +1162 -1120
- package/content/dev/skills/fluent-rollback/SKILL.md +387 -0
- package/content/dev/skills/fluent-rule-scaffold/SKILL.md +515 -394
- package/content/dev/skills/fluent-scope-decompose/SKILL.md +1123 -1021
- package/content/dev/skills/fluent-session-audit-export/SKILL.md +880 -632
- package/content/dev/skills/fluent-session-summary/SKILL.md +320 -195
- package/content/dev/skills/fluent-settings/SKILL.md +151 -2
- package/content/dev/skills/fluent-source-onboard/SKILL.md +23 -4
- package/content/dev/skills/fluent-sourcing/SKILL.md +14 -0
- package/content/dev/skills/fluent-system-monitoring/SKILL.md +771 -767
- package/content/dev/skills/fluent-test-data/SKILL.md +514 -513
- package/content/dev/skills/fluent-trace/SKILL.md +1169 -1143
- package/content/dev/skills/fluent-transition-api/SKILL.md +364 -346
- package/content/dev/skills/fluent-use-case-discover/SKILL.md +593 -471
- package/content/dev/skills/fluent-use-case-discover/SPEC_TEMPLATE.md +22 -1
- package/content/dev/skills/fluent-version-manage/SKILL.md +44 -3
- package/content/dev/skills/fluent-workflow-analyzer/SKILL.md +995 -959
- package/content/dev/skills/fluent-workflow-builder/SKILL.md +668 -326
- package/content/dev/skills/fluent-workflow-deploy/SKILL.md +480 -0
- package/content/dev/skills/fluent-workspace-tree/SKILL.md +281 -0
- package/content/mcp-extn/agents/fluent-mcp.md +133 -132
- package/content/mcp-extn/skills/fluent-mcp-tools/SKILL.md +812 -800
- package/content/mcp-official/agents/fluent-mcp-core.md +91 -91
- package/content/mcp-official/skills/fluent-mcp-core/SKILL.md +94 -94
- package/content/rfl/skills/fluent-rfl-assess/SKILL.md +172 -172
- package/docs/CAPABILITY_MAP.md +106 -73
- package/docs/DEPLOYMENT_PROMOTION_RUNBOOK.md +218 -0
- package/docs/DESIGN-implementation-map.md +698 -0
- package/docs/DEV_WORKFLOW.md +814 -802
- package/docs/FLOW_RUN.md +142 -142
- package/docs/GETTING_STARTED.md +427 -0
- package/docs/USE_CASES.md +906 -50
- package/metadata.json +184 -155
- package/package.json +7 -2
- package/docs/USE_CASES.pdf +0 -0
|
@@ -0,0 +1,1830 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fluent-mystique-scaffold
|
|
3
|
+
description: Scaffold a new Mystique SDK component project with webpack, TypeScript, Storybook, Jest, component registration, and i18n — ready to develop and deploy. Triggers on "scaffold component", "new mystique project", "create sdk component", "mystique sdk setup", "new ui plugin".
|
|
4
|
+
user-invocable: true
|
|
5
|
+
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
|
|
6
|
+
argument-hint: <project-name> [--components ComponentA,ComponentB] [--category content|layout|page|filter]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Mystique SDK Component Scaffolder
|
|
10
|
+
|
|
11
|
+
Generate a complete, buildable Mystique SDK component project from a project name and optional component list. The output is a directory under `accounts/<PROFILE>/SOURCE/` containing a React/TypeScript/webpack project that produces a `bundle.[contenthash].js` file loadable via the `plugins[]` array in any Mystique manifest.
|
|
12
|
+
|
|
13
|
+
A Mystique SDK project is a standalone React application that registers custom UI components with the Mystique ComponentRegistry. At runtime, the Fluent OMX host application loads the bundle from a URL, executes it, and the registered components become available for use in manifests alongside built-in `fc.*` components. The host application provides React, ReactDOM, Material-UI, and Mystique globals as webpack externals -- the bundle does NOT include these libraries.
|
|
14
|
+
|
|
15
|
+
## Pre-Check: New Project or Extend Existing?
|
|
16
|
+
|
|
17
|
+
**ALWAYS run this decision tree before scaffolding. Do NOT skip.**
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
User asks: "I need a UI component" / "Create a Mystique plugin" / "Build a custom component"
|
|
21
|
+
|
|
|
22
|
+
+-- 1. Discover existing SDK projects in workspace
|
|
23
|
+
| Search: accounts/<PROFILE>/SOURCE/*/package.json (look for mystique deps)
|
|
24
|
+
| Search: accounts/<PROFILE>/SOURCE/*/webpack.config.*
|
|
25
|
+
| Search: accounts/<PROFILE>/SOURCE/*/@types/mystique/
|
|
26
|
+
| List found projects with their registered components
|
|
27
|
+
|
|
|
28
|
+
+-- 2. Check deployed plugins in live manifests
|
|
29
|
+
| Look for plugins[] array entries in deployed manifests
|
|
30
|
+
| Cross-reference with local SOURCE/
|
|
31
|
+
|
|
|
32
|
+
+-- 3. Evaluate: does the new component fit an existing project?
|
|
33
|
+
| +-- YES -> Add component to existing project (just generate component files)
|
|
34
|
+
| +-- NO -> Genuinely new domain, proceed with full scaffolding
|
|
35
|
+
| +-- UNCLEAR -> Ask user
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Source Code Availability Check
|
|
39
|
+
|
|
40
|
+
Before extending an existing project, verify the source is accessible:
|
|
41
|
+
|
|
42
|
+
| Source State | Action |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `accounts/<PROFILE>/SOURCE/<project>/` exists with `package.json` + `@types/mystique/` | Ready -- add component files directly |
|
|
45
|
+
| Plugin deployed but no source in `SOURCE/` | Ask user to clone repo into `SOURCE/` |
|
|
46
|
+
| Source exists but registered components unknown | Read `src/index.tsx` to discover existing registrations |
|
|
47
|
+
|
|
48
|
+
## Planning Gate
|
|
49
|
+
|
|
50
|
+
### Pre-flight: Plan Verification
|
|
51
|
+
|
|
52
|
+
Before proceeding, check for an existing approved plan:
|
|
53
|
+
|
|
54
|
+
1. **Search** `accounts/<PROFILE>/features/*/status.json` for an entry with `plan: "APPROVED"` that covers this UI work, OR check `accounts/<PROFILE>/tasks/` for a task plan with `Status: APPROVED`
|
|
55
|
+
2. **If multi-artifact work** (SDK component + manifest + workflows + settings): STOP. Invoke `/fluent-feature-plan` first -- this skill cannot be used for multi-artifact work without a feature plan
|
|
56
|
+
3. **If approved plan found:** Skip to implementation, referencing the plan's UI sections
|
|
57
|
+
4. **If standalone SDK project work with no plan:** Continue to the Planning Gate below to write a plan for this skill
|
|
58
|
+
|
|
59
|
+
**After the pre-check determines a NEW project is needed, write a plan.** Same pattern as `/fluent-module-scaffold`.
|
|
60
|
+
|
|
61
|
+
**Mystique-scaffold specific emphasis -- ensure these are covered:**
|
|
62
|
+
|
|
63
|
+
1. **Component inventory** -- names, aliases, categories, props interface for each component
|
|
64
|
+
2. **Data dependencies** -- what GraphQL entities/queries each component will need
|
|
65
|
+
3. **Styling approach** -- Material-UI theme extension or custom CSS via Emotion
|
|
66
|
+
4. **Integration points** -- which manifest pages will use these components
|
|
67
|
+
5. **Testing strategy** -- unit tests, Storybook stories, Cypress E2E
|
|
68
|
+
6. **Hosting strategy** -- where the production bundle will be deployed (Vercel, S3, CDN)
|
|
69
|
+
|
|
70
|
+
**Write the plan to:** `accounts/<PROFILE>/tasks/<YYYY-MM-DD>-mystique-scaffold-<slug>.md`. Set `Status: PENDING`.
|
|
71
|
+
|
|
72
|
+
Present the full plan content to the user and wait for approval before generating any files. On approval, update the file to `Status: APPROVED`. If the user says "just do it", proceed directly (still write the file for audit trail).
|
|
73
|
+
|
|
74
|
+
## Handoff Protocol
|
|
75
|
+
|
|
76
|
+
### Signals emitted by this skill
|
|
77
|
+
|
|
78
|
+
| Signal | Condition | Example |
|
|
79
|
+
|--------|-----------|---------|
|
|
80
|
+
| `-> READY: <path>` | Project directory scaffolded | `-> READY: accounts/SAGIRISH/SOURCE/mystique-plugin-curbside/` |
|
|
81
|
+
| `-> NEXT: /fluent-<skill>` | Ready for manifest wiring | `-> NEXT: /fluent-mystique-builder` |
|
|
82
|
+
| `-> BLOCKED: <reason>` | Cannot proceed | `-> BLOCKED: PLAN_REQUIRED -- Write a plan via /fluent-feature-plan` |
|
|
83
|
+
| `-> SKIP: <reason>` | Existing project covers the domain | `-> SKIP: Extend existing project mystique-plugin-hm. Add component files directly` |
|
|
84
|
+
|
|
85
|
+
### Error codes
|
|
86
|
+
|
|
87
|
+
| Code | Condition | Recovery |
|
|
88
|
+
|------|-----------|----------|
|
|
89
|
+
| `PLAN_REQUIRED` | Scaffolding attempted without approved plan | Run `/fluent-feature-plan` first |
|
|
90
|
+
| `VALIDATION_FAILED` | Project name conflicts or invalid component names | Fix input parameters |
|
|
91
|
+
|
|
92
|
+
## When to Use
|
|
93
|
+
|
|
94
|
+
- Creating a brand-new Mystique SDK component project from scratch
|
|
95
|
+
- Starting a greenfield UI plugin that needs webpack, TypeScript, Storybook, Jest, and component registration
|
|
96
|
+
- Bootstrapping a component project for a new feature requiring custom UI beyond built-in `fc.*` components
|
|
97
|
+
- Generating a project shell to be populated incrementally with new components
|
|
98
|
+
|
|
99
|
+
## Ownership Boundary
|
|
100
|
+
|
|
101
|
+
This skill owns:
|
|
102
|
+
|
|
103
|
+
- Creating the complete SDK project directory structure
|
|
104
|
+
- Generating `package.json` with correct dependencies and scripts
|
|
105
|
+
- Generating `webpack.config.ts` with proper externals (React, ReactDOM, Material-UI, Mystique)
|
|
106
|
+
- Generating `tsconfig.json` with strict TypeScript configuration
|
|
107
|
+
- Generating `jest.config.js` with ts-jest, mocks, and coverage thresholds
|
|
108
|
+
- Generating `.babelrc` with React/TypeScript/Emotion presets
|
|
109
|
+
- Copying `@types/mystique/` type definitions (7 files)
|
|
110
|
+
- Generating `lib/` SDK initialization files (init.js, mystique.js, mystique-export.js, material-ui.js)
|
|
111
|
+
- Generating Storybook configuration (`.storybook/main.ts`, `preview.js`, `helpers.ts`)
|
|
112
|
+
- Generating component files with test and story for each `--components` entry
|
|
113
|
+
- Generating `src/index.tsx` with ComponentRegistry registrations
|
|
114
|
+
- Generating example manifest in `manifests/`
|
|
115
|
+
- Generating `src/index.html` for dev server
|
|
116
|
+
- Generating `.gitignore`, `.eslintrc.js`
|
|
117
|
+
|
|
118
|
+
This skill does **not** own:
|
|
119
|
+
|
|
120
|
+
- Creating or editing Mystique manifests --> `/fluent-mystique-builder`
|
|
121
|
+
- Validating manifest JSON --> `/fluent-mystique-validate`
|
|
122
|
+
- Analyzing existing manifests --> `/fluent-mystique-analyze`
|
|
123
|
+
- Deploying manifests as settings --> `/fluent-settings`
|
|
124
|
+
- Building/deploying Java modules --> `/fluent-build`, `/fluent-module-deploy`
|
|
125
|
+
|
|
126
|
+
## Inputs
|
|
127
|
+
|
|
128
|
+
| Parameter | Required | Default | Description |
|
|
129
|
+
|-----------|----------|---------|-------------|
|
|
130
|
+
| `project-name` | Yes | -- | Project directory name (e.g., `mystique-plugin-curbside`). Used for directory name and package.json name. Lowercase alphanumeric with hyphens. |
|
|
131
|
+
| `--components` | No | -- | Comma-separated PascalCase component names to scaffold (e.g., `OrderTracker,StoreMap`). Each gets a `.tsx`, `.test.tsx`, `.stories.tsx`, and `index.ts`. |
|
|
132
|
+
| `--category` | No | `content` | Default component category for ComponentRegistry registration. Valid: `page`, `layout`, `content`, `filter`. |
|
|
133
|
+
| `--namespace` | No | Auto-detect from project name | Component alias prefix (e.g., `acme` produces aliases like `acme.content.order-tracker`). |
|
|
134
|
+
| `--profile` | No | Active profile | Fluent CLI profile for path resolution |
|
|
135
|
+
|
|
136
|
+
## Pre-Check: Project Already Exists?
|
|
137
|
+
|
|
138
|
+
Before creating anything, check if the target directory already exists:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
accounts/<PROFILE>/SOURCE/<project-name>/
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
If it exists, **abort** with message:
|
|
145
|
+
```
|
|
146
|
+
Project directory already exists: accounts/<PROFILE>/SOURCE/<project-name>/
|
|
147
|
+
Use this skill with --components to add components to the existing project, or choose a different name.
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Namespace Detection
|
|
151
|
+
|
|
152
|
+
The namespace is used in ComponentRegistry aliases following the pattern `<namespace>.<category>.<kebab-name>`. Detection order:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
1. If --namespace provided, use it directly
|
|
156
|
+
|
|
157
|
+
2. Else derive from project-name:
|
|
158
|
+
mystique-plugin-curbside -> curbside
|
|
159
|
+
mystique-plugin-hm-custom -> hm
|
|
160
|
+
fluent-component-acme -> acme
|
|
161
|
+
Strip "mystique-plugin-" or "fluent-component-" prefix, take first word
|
|
162
|
+
|
|
163
|
+
3. Fallback: "custom"
|
|
164
|
+
Warn user: "Could not derive namespace. Components will use 'custom.*' aliases."
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Generated Directory Structure
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
accounts/<PROFILE>/SOURCE/<project-name>/
|
|
171
|
+
+-- package.json # Dependencies, scripts, browserslist
|
|
172
|
+
+-- tsconfig.json # TypeScript config (strict mode, ES6 target)
|
|
173
|
+
+-- webpack.config.ts # Bundle output with content hash, externals
|
|
174
|
+
+-- jest.config.js # Jest with ts-jest, coverage thresholds
|
|
175
|
+
+-- .babelrc # Babel presets for React/TypeScript/Emotion
|
|
176
|
+
+-- .eslintrc.js # ESLint config for TypeScript + React
|
|
177
|
+
+-- .gitignore
|
|
178
|
+
+-- sdk-version.json # SDK version tracking
|
|
179
|
+
+-- .storybook/
|
|
180
|
+
| +-- main.ts # Storybook webpack config with externals
|
|
181
|
+
| +-- preview.js # Decorators, viewport config
|
|
182
|
+
| +-- helpers.ts # Story template utility
|
|
183
|
+
| +-- Storyshots.test.ts # Snapshot testing for stories
|
|
184
|
+
+-- @types/
|
|
185
|
+
| +-- images.d.ts # Image import declarations
|
|
186
|
+
| +-- mystique/ # Mystique SDK type definitions
|
|
187
|
+
| +-- index.d.ts # Common types (QueryResponse, Connection, Edge)
|
|
188
|
+
| +-- manifest.d.ts # Full manifest schema types (MystiqueManifest, Route, Page, Section)
|
|
189
|
+
| +-- components.d.ts # Component prop interfaces (Card, List, Children, DynamicValue, Loading)
|
|
190
|
+
| +-- hooks.d.ts # Hook definitions (useAuth, useQuery, useSettings, useI18n, useData, useRest, useUserActions)
|
|
191
|
+
| +-- services.d.ts # Service types (MystiqueThemeColours)
|
|
192
|
+
| +-- registry.d.ts # Registry interfaces (ComponentRegistry, FieldRegistry, TemplateRegistry)
|
|
193
|
+
| +-- frame.d.ts # Modal/Drawer/Toast utilities (pushModal, pushDrawer, pushToast)
|
|
194
|
+
+-- i18n/
|
|
195
|
+
| +-- locales/
|
|
196
|
+
| +-- en/
|
|
197
|
+
| +-- translation.json # Initial i18n keys
|
|
198
|
+
+-- lib/
|
|
199
|
+
| +-- init.js # Global setup: React, ReactDOM, MaterialUI, Emotion on globalThis
|
|
200
|
+
| +-- material-ui.js # MaterialUI UMD global (bundled for Storybook/Jest)
|
|
201
|
+
| +-- mystique.js # Mystique global object (bundled for Storybook/Jest)
|
|
202
|
+
| +-- mystique-export.js # Named exports from globalThis.Mystique for import resolution
|
|
203
|
+
+-- src/
|
|
204
|
+
| +-- index.tsx # Entry point -- ComponentRegistry.register() calls
|
|
205
|
+
| +-- index.html # Dev server HTML with CDN React/ReactDOM
|
|
206
|
+
| +-- setup-tests.ts # Jest setup (Emotion matchers)
|
|
207
|
+
| +-- components/
|
|
208
|
+
| | +-- <ComponentName>/
|
|
209
|
+
| | +-- <ComponentName>.tsx # Component implementation
|
|
210
|
+
| | +-- <ComponentName>.test.tsx # Unit test
|
|
211
|
+
| | +-- <ComponentName>.stories.tsx # Storybook story
|
|
212
|
+
| | +-- index.ts # Re-export
|
|
213
|
+
| +-- utils/
|
|
214
|
+
| +-- GqlUtils.ts # GraphQL document-to-string utility
|
|
215
|
+
| +-- TestUtils.tsx # MaterialUI classname fix for test snapshots
|
|
216
|
+
+-- manifests/
|
|
217
|
+
| +-- fc.mystique.manifest.example.ts # Example full manifest
|
|
218
|
+
| +-- fc.mystique.manifest.fragment.example.ts # Example fragment manifest
|
|
219
|
+
+-- build/
|
|
220
|
+
| +-- GenerateVersionPlugin.ts # Webpack plugin: emits version.json with bundle path
|
|
221
|
+
| +-- GenerateManifestsPlugin.ts # Webpack plugin: compiles manifest .ts to .json in dist/
|
|
222
|
+
+-- cypress/ # (empty scaffold, ready for E2E tests)
|
|
223
|
+
| +-- cypress.json
|
|
224
|
+
+-- dist/ # (empty, webpack output directory)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## File Templates
|
|
228
|
+
|
|
229
|
+
### package.json
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{
|
|
233
|
+
"name": "${PROJECT_NAME}",
|
|
234
|
+
"version": "1.0.0",
|
|
235
|
+
"description": "Mystique SDK Plugin - ${PROJECT_TITLE}",
|
|
236
|
+
"main": "index.js",
|
|
237
|
+
"scripts": {
|
|
238
|
+
"start": "webpack serve --mode development --open --hot --port 3001",
|
|
239
|
+
"build": "webpack --mode production && jest ./build",
|
|
240
|
+
"bundle": "webpack",
|
|
241
|
+
"test": "jest --coverage --testPathIgnorePatterns ./build/**",
|
|
242
|
+
"test:bail": "jest --bail --testPathIgnorePatterns ./build/**",
|
|
243
|
+
"test:storybook": "start-storybook --smoke-test",
|
|
244
|
+
"check-types": "tsc",
|
|
245
|
+
"storybook": "start-storybook -p 6006 -s ./lib",
|
|
246
|
+
"storybook:build": "build-storybook -o dist/_storybook && cp ./lib/*.js ./dist/_storybook",
|
|
247
|
+
"lint": "eslint . --ext .ts,.tsx",
|
|
248
|
+
"lint:fix": "yarn run lint -- --fix"
|
|
249
|
+
},
|
|
250
|
+
"devDependencies": {
|
|
251
|
+
"@babel/core": "^7.13.8",
|
|
252
|
+
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
|
253
|
+
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
|
254
|
+
"@babel/preset-env": "^7.13.9",
|
|
255
|
+
"@babel/preset-react": "^7.12.10",
|
|
256
|
+
"@babel/preset-typescript": "^7.12.7",
|
|
257
|
+
"@emotion/babel-preset-css-prop": "^11.10.0",
|
|
258
|
+
"@emotion/eslint-plugin": "^11.10.0",
|
|
259
|
+
"@emotion/jest": "^11.10.0",
|
|
260
|
+
"@emotion/react": "^11.10.0",
|
|
261
|
+
"@storybook/addon-actions": "6.5.12",
|
|
262
|
+
"@storybook/addon-essentials": "6.5.12",
|
|
263
|
+
"@storybook/addon-links": "6.5.12",
|
|
264
|
+
"@storybook/addon-storyshots": "6.5.12",
|
|
265
|
+
"@storybook/builder-webpack5": "6.5.12",
|
|
266
|
+
"@storybook/core-common": "6.5.12",
|
|
267
|
+
"@storybook/manager-webpack5": "6.5.12",
|
|
268
|
+
"@storybook/react": "6.5.12",
|
|
269
|
+
"@testing-library/dom": "^7.31.2",
|
|
270
|
+
"@testing-library/jest-dom": "^5.11.9",
|
|
271
|
+
"@testing-library/react": "^11.2.5",
|
|
272
|
+
"@testing-library/react-hooks": "^5.1.1",
|
|
273
|
+
"@testing-library/user-event": "^13.1.9",
|
|
274
|
+
"@types/jest": "^26.0.20",
|
|
275
|
+
"@types/lodash": "^4.14.167",
|
|
276
|
+
"@types/material-ui": "^0.21.8",
|
|
277
|
+
"@types/node": "^18.7.14",
|
|
278
|
+
"@types/react": "^17.0.0",
|
|
279
|
+
"@types/react-dom": "^17.0.0",
|
|
280
|
+
"@types/testing-library__jest-dom": "^5.14.0",
|
|
281
|
+
"@types/webpack": "^5.0.0",
|
|
282
|
+
"@types/webpack-dev-server": "^3.11.3",
|
|
283
|
+
"@typescript-eslint/eslint-plugin": "^4.28.0",
|
|
284
|
+
"@typescript-eslint/parser": "^4.28.0",
|
|
285
|
+
"babel-jest": "^26.6.3",
|
|
286
|
+
"babel-loader": "^8.2.2",
|
|
287
|
+
"copy-webpack-plugin": "^9.0.1",
|
|
288
|
+
"eslint": "^7.29.0",
|
|
289
|
+
"eslint-plugin-no-only-tests": "^2.6.0",
|
|
290
|
+
"eslint-plugin-react": "^7.24.0",
|
|
291
|
+
"eslint-plugin-react-hooks": "^4.2.0",
|
|
292
|
+
"graphql": "^15.4.0",
|
|
293
|
+
"graphql-tag": "^2.12.4",
|
|
294
|
+
"html-webpack-plugin": "^5.3.1",
|
|
295
|
+
"jest": "^26.6.3",
|
|
296
|
+
"jest-fetch-mock": "^3.0.3",
|
|
297
|
+
"jest-html-reporter": "^3.3.0",
|
|
298
|
+
"lodash": "^4.17.21",
|
|
299
|
+
"react-test-renderer": "^17.0.2",
|
|
300
|
+
"ts-jest": "^26.5.2",
|
|
301
|
+
"typescript": "^4.1.3",
|
|
302
|
+
"url-loader": "^4.1.1",
|
|
303
|
+
"webpack": "^5.74.0",
|
|
304
|
+
"webpack-cli": "^4.10.0",
|
|
305
|
+
"webpack-dev-server": "^4.10.0"
|
|
306
|
+
},
|
|
307
|
+
"dependencies": {
|
|
308
|
+
"@material-ui/core": "4.12.1",
|
|
309
|
+
"@material-ui/pickers": "^3.3.10",
|
|
310
|
+
"qs": "^6.10.1",
|
|
311
|
+
"react": "^17.0.1",
|
|
312
|
+
"react-dom": "^17.0.1"
|
|
313
|
+
},
|
|
314
|
+
"browserslist": {
|
|
315
|
+
"production": [">0.2%", "not dead", "not op_mini all"],
|
|
316
|
+
"development": ["last 1 chrome version", "last 1 firefox version", "last 1 safari version"]
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Important notes about dependencies:**
|
|
322
|
+
- `react`, `react-dom`, and `@material-ui/core` are listed as `dependencies` but are treated as webpack **externals** -- they are NOT bundled into the output. They exist in `dependencies` for TypeScript type resolution and for Storybook/Jest test execution.
|
|
323
|
+
- The host Fluent OMX application provides React 17, ReactDOM 17, and Material-UI 4.12 at runtime via `window.React`, `window.ReactDOM`, and `window.MaterialUI` globals.
|
|
324
|
+
- Storybook version is pinned to 6.5.12 for compatibility with the Mystique SDK.
|
|
325
|
+
|
|
326
|
+
### webpack.config.ts
|
|
327
|
+
|
|
328
|
+
This is the most critical file -- it controls how the bundle is produced and what is externalized.
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
const path = require('path');
|
|
332
|
+
import { Configuration } from 'webpack';
|
|
333
|
+
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
|
334
|
+
const GenerateVersionPlugin = require('./build/GenerateVersionPlugin');
|
|
335
|
+
const GenerateManifestsPlugin = require('./build/GenerateManifestsPlugin');
|
|
336
|
+
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
|
337
|
+
|
|
338
|
+
const config: Configuration = {
|
|
339
|
+
|
|
340
|
+
// Entry point -- all ComponentRegistry.register() calls happen here
|
|
341
|
+
entry: './src/index',
|
|
342
|
+
|
|
343
|
+
devServer: {
|
|
344
|
+
allowedHosts: 'all',
|
|
345
|
+
static: {
|
|
346
|
+
directory: path.resolve(__dirname, 'dist'),
|
|
347
|
+
watch: true,
|
|
348
|
+
},
|
|
349
|
+
headers: {
|
|
350
|
+
'Access-Control-Allow-Origin': '*',
|
|
351
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
|
352
|
+
'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
// Output: bundle.[contenthash].js for cache busting
|
|
357
|
+
output: {
|
|
358
|
+
path: path.join(__dirname, '/dist'),
|
|
359
|
+
clean: true,
|
|
360
|
+
filename: 'bundle.[contenthash].js',
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
resolve: {
|
|
364
|
+
extensions: ['.ts', '.tsx', '.js'],
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
module: {
|
|
368
|
+
rules: [
|
|
369
|
+
{
|
|
370
|
+
test: /\.(ts|js)x?$/,
|
|
371
|
+
exclude: /node_modules/,
|
|
372
|
+
use: {
|
|
373
|
+
loader: 'babel-loader',
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
test: /\.(png|jp(e*)g|svg)$/,
|
|
378
|
+
use: [{
|
|
379
|
+
loader: 'url-loader',
|
|
380
|
+
options: {
|
|
381
|
+
limit: 8000,
|
|
382
|
+
name: 'images/[hash]-[name].[ext]',
|
|
383
|
+
},
|
|
384
|
+
}],
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
},
|
|
388
|
+
plugins: [
|
|
389
|
+
new HtmlWebpackPlugin({
|
|
390
|
+
template: './src/index.html',
|
|
391
|
+
}),
|
|
392
|
+
new CopyWebpackPlugin({
|
|
393
|
+
patterns: [
|
|
394
|
+
{
|
|
395
|
+
from: 'i18n/locales',
|
|
396
|
+
to: path.join(__dirname, 'dist/i18n'),
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
}),
|
|
400
|
+
new GenerateVersionPlugin(),
|
|
401
|
+
new GenerateManifestsPlugin(),
|
|
402
|
+
],
|
|
403
|
+
|
|
404
|
+
// CRITICAL: externals configuration
|
|
405
|
+
// React, ReactDOM, Emotion, Material-UI, and Mystique are provided by the host app
|
|
406
|
+
// They must NOT be bundled -- the bundle uses window globals at runtime
|
|
407
|
+
externals: [{
|
|
408
|
+
'react': 'React',
|
|
409
|
+
'react-dom': 'ReactDOM',
|
|
410
|
+
'@emotion/react': 'emotionReact',
|
|
411
|
+
},
|
|
412
|
+
externalMaterialUI,
|
|
413
|
+
externalMystique,
|
|
414
|
+
],
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/** Re-route @material-ui/core imports to the global UMD import */
|
|
418
|
+
export function externalMaterialUI (_: any, module: any, callback: any): any {
|
|
419
|
+
if (module === '@material-ui/core') {
|
|
420
|
+
return callback(null, `window["MaterialUI"]`);
|
|
421
|
+
}
|
|
422
|
+
const isMaterialUIComponent = /^@material-ui\/core\/.+$/;
|
|
423
|
+
const match = isMaterialUIComponent.exec(module);
|
|
424
|
+
if (match !== null) {
|
|
425
|
+
const parts = module.split('/');
|
|
426
|
+
const component = parts[parts.length - 1];
|
|
427
|
+
if (component === 'styles') {
|
|
428
|
+
return callback(null, `window["MaterialUI"]`);
|
|
429
|
+
}
|
|
430
|
+
return callback(null, `window["MaterialUI"].${component}`);
|
|
431
|
+
}
|
|
432
|
+
callback();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Re-route mystique/* imports to the global Mystique object */
|
|
436
|
+
export function externalMystique (_: any, module: any, callback: any): any {
|
|
437
|
+
const isMystiqueComponent = /^mystique\/.+$/;
|
|
438
|
+
const match = isMystiqueComponent.exec(module);
|
|
439
|
+
if (match !== null) {
|
|
440
|
+
return callback(null, `window["Mystique"]`);
|
|
441
|
+
}
|
|
442
|
+
callback();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export default config;
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**Key points about the webpack config:**
|
|
449
|
+
- `CORS headers` on devServer are required so the Fluent OMX host app (running on a different port/domain) can load the bundle.
|
|
450
|
+
- `contenthash` in the filename enables cache busting in production.
|
|
451
|
+
- The `GenerateVersionPlugin` emits a `version.json` that maps the hashed bundle filename, enabling the host app to auto-discover the current bundle.
|
|
452
|
+
- The `GenerateManifestsPlugin` compiles `.ts` manifest files under `manifests/` into `.json` in `dist/manifests/`.
|
|
453
|
+
- The three external handlers ensure ZERO framework code ends up in the bundle. The host OMX app provides everything via `window` globals.
|
|
454
|
+
|
|
455
|
+
### tsconfig.json
|
|
456
|
+
|
|
457
|
+
```json
|
|
458
|
+
{
|
|
459
|
+
"compilerOptions": {
|
|
460
|
+
"target": "es6",
|
|
461
|
+
"outDir": "./dist/",
|
|
462
|
+
"strictNullChecks": true,
|
|
463
|
+
"moduleResolution": "node",
|
|
464
|
+
"resolveJsonModule": true,
|
|
465
|
+
"allowJs": true,
|
|
466
|
+
"noEmit": true,
|
|
467
|
+
"strict": true,
|
|
468
|
+
"esModuleInterop": true,
|
|
469
|
+
"jsx": "react-jsx",
|
|
470
|
+
"jsxImportSource": "@emotion/react",
|
|
471
|
+
"baseUrl": "./src",
|
|
472
|
+
"lib": ["es2015", "dom.iterable", "es2016.array.include", "es2017.object", "dom"],
|
|
473
|
+
"types": ["node", "jest", "@testing-library/jest-dom"],
|
|
474
|
+
"typeRoots": ["./node_modules/@types", "./@types/**/*"],
|
|
475
|
+
"module": "es6",
|
|
476
|
+
"removeComments": true,
|
|
477
|
+
"alwaysStrict": true,
|
|
478
|
+
"allowUnreachableCode": false,
|
|
479
|
+
"noImplicitAny": true,
|
|
480
|
+
"noImplicitThis": true,
|
|
481
|
+
"noUnusedLocals": true,
|
|
482
|
+
"noUnusedParameters": true,
|
|
483
|
+
"noImplicitReturns": true,
|
|
484
|
+
"noFallthroughCasesInSwitch": true,
|
|
485
|
+
"forceConsistentCasingInFileNames": true,
|
|
486
|
+
"importHelpers": true,
|
|
487
|
+
"skipLibCheck": true
|
|
488
|
+
},
|
|
489
|
+
"include": ["src/**/*", "manifests/**/*", "@types/**/*", "./@types"],
|
|
490
|
+
"exclude": ["node_modules", "./node_modules", "./node_modules/*", "./node_modules/@types/node/index.d.ts"]
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**Key: `jsxImportSource: "@emotion/react"`** enables Emotion CSS-in-JS props on JSX elements. The `typeRoots` includes `./@types/**/*` which is where the Mystique type definitions live.
|
|
495
|
+
|
|
496
|
+
### jest.config.js
|
|
497
|
+
|
|
498
|
+
```javascript
|
|
499
|
+
const path = require('path');
|
|
500
|
+
|
|
501
|
+
module.exports = {
|
|
502
|
+
restoreMocks: true,
|
|
503
|
+
roots: [
|
|
504
|
+
"<rootDir>"
|
|
505
|
+
],
|
|
506
|
+
setupFilesAfterSetup: ['./src/setup-tests.ts'],
|
|
507
|
+
setupFiles: [
|
|
508
|
+
"./lib/material-ui.js",
|
|
509
|
+
"./lib/init.js",
|
|
510
|
+
"./lib/mystique.js",
|
|
511
|
+
],
|
|
512
|
+
testMatch: [
|
|
513
|
+
"**/?(*.)+(test).+(ts|tsx)"
|
|
514
|
+
],
|
|
515
|
+
coverageThreshold: {
|
|
516
|
+
global: {
|
|
517
|
+
statements: 80,
|
|
518
|
+
branches: 80,
|
|
519
|
+
functions: 80,
|
|
520
|
+
lines: 80
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
coveragePathIgnorePatterns: [
|
|
524
|
+
".storybook/",
|
|
525
|
+
"src/index.tsx",
|
|
526
|
+
'.stories.'
|
|
527
|
+
],
|
|
528
|
+
collectCoverageFrom: [
|
|
529
|
+
"src/**/*.{ts,tsx}"
|
|
530
|
+
],
|
|
531
|
+
preset: 'ts-jest',
|
|
532
|
+
transform: {
|
|
533
|
+
'^.+\\.(ts|tsx)?$': 'ts-jest',
|
|
534
|
+
"^.+\\.(js|jsx)$": "babel-jest",
|
|
535
|
+
},
|
|
536
|
+
moduleNameMapper: {
|
|
537
|
+
"\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "identity-obj-proxy",
|
|
538
|
+
"\\.(css|less)$": "identity-obj-proxy",
|
|
539
|
+
"^./react": path.resolve(__dirname, 'node_modules/react'),
|
|
540
|
+
"^mystique": path.resolve(__dirname, 'lib/mystique-export.js')
|
|
541
|
+
},
|
|
542
|
+
reporters: [
|
|
543
|
+
"default",
|
|
544
|
+
["./node_modules/jest-html-reporter", {
|
|
545
|
+
"pageTitle": "Test Report",
|
|
546
|
+
"outputPath": "./test-report/test-report.html"
|
|
547
|
+
}]
|
|
548
|
+
],
|
|
549
|
+
};
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
**Critical: `setupFiles`** loads `lib/material-ui.js`, `lib/init.js`, and `lib/mystique.js` before tests run. These files set up the global objects (`window.React`, `window.MaterialUI`, `window.Mystique`) that the webpack externals resolve to at runtime. The `moduleNameMapper` for `^mystique` redirects Mystique module imports to `lib/mystique-export.js` which provides named exports from the global Mystique object.
|
|
553
|
+
|
|
554
|
+
### .babelrc
|
|
555
|
+
|
|
556
|
+
```json
|
|
557
|
+
{
|
|
558
|
+
"presets": [
|
|
559
|
+
"@babel/preset-env",
|
|
560
|
+
"@babel/preset-typescript",
|
|
561
|
+
[
|
|
562
|
+
"@babel/preset-react",
|
|
563
|
+
{
|
|
564
|
+
"runtime": "automatic"
|
|
565
|
+
}
|
|
566
|
+
],
|
|
567
|
+
"@emotion/babel-preset-css-prop"
|
|
568
|
+
],
|
|
569
|
+
"plugins": ["@emotion", "@babel/proposal-class-properties", "@babel/proposal-object-rest-spread"]
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### lib/init.js
|
|
574
|
+
|
|
575
|
+
Sets up global objects that the webpack externals resolve to. Required for both Jest tests and the dev server HTML page.
|
|
576
|
+
|
|
577
|
+
```javascript
|
|
578
|
+
import fetchMock from 'jest-fetch-mock';
|
|
579
|
+
fetchMock.enableMocks();
|
|
580
|
+
|
|
581
|
+
import React from 'react';
|
|
582
|
+
globalThis.React = React;
|
|
583
|
+
|
|
584
|
+
import ReactDOM from 'react-dom';
|
|
585
|
+
globalThis.ReactDOM = ReactDOM;
|
|
586
|
+
|
|
587
|
+
import * as MaterialUI from '@material-ui/core';
|
|
588
|
+
globalThis.MaterialUI = MaterialUI;
|
|
589
|
+
|
|
590
|
+
import * as Emotion from '@emotion/react';
|
|
591
|
+
globalThis.emotionReact = Emotion;
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### lib/mystique-export.js
|
|
595
|
+
|
|
596
|
+
Provides named exports from the global `Mystique` object. This file is what `moduleNameMapper` in Jest points `mystique/*` imports to, and what makes hook/component imports work in tests.
|
|
597
|
+
|
|
598
|
+
```javascript
|
|
599
|
+
const m = globalThis.Mystique;
|
|
600
|
+
|
|
601
|
+
// registry
|
|
602
|
+
export const ComponentRegistry = m.ComponentRegistry;
|
|
603
|
+
export const FieldRegistry = m.FieldRegistry;
|
|
604
|
+
|
|
605
|
+
// frame
|
|
606
|
+
export const DataModal = m.DataModal;
|
|
607
|
+
export const pushModal = m.pushModal;
|
|
608
|
+
export const pushDrawer = m.pushDrawer;
|
|
609
|
+
export const pushToast = m.pushToast;
|
|
610
|
+
|
|
611
|
+
// hooks
|
|
612
|
+
export const useAuth = m.useAuth;
|
|
613
|
+
export const useI18n = m.useI18n;
|
|
614
|
+
export const useSettings = m.useSettings;
|
|
615
|
+
export const useQuery = m.useQuery;
|
|
616
|
+
export const useMutation = m.useMutation;
|
|
617
|
+
export const useRest = m.useRest;
|
|
618
|
+
export const getQuery = m.getQuery;
|
|
619
|
+
export const getRest = m.getRest;
|
|
620
|
+
|
|
621
|
+
// components
|
|
622
|
+
export const Card = m.Card;
|
|
623
|
+
export const CardContent = m.CardContent;
|
|
624
|
+
export const Children = m.Children;
|
|
625
|
+
export const DynamicValue = m.DynamicValue;
|
|
626
|
+
export const Loading = m.Loading;
|
|
627
|
+
export const QuantitySelectorComponent = m.QuantitySelectorComponent;
|
|
628
|
+
export const QuantityList = m.QuantityList;
|
|
629
|
+
|
|
630
|
+
// utils
|
|
631
|
+
export const decorateQueryResult = m.decorateQueryResult;
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### lib/mystique.js and lib/material-ui.js
|
|
635
|
+
|
|
636
|
+
These are large bundled files from the Fluent Commerce SDK distribution. They set up `globalThis.Mystique` and `globalThis.MaterialUI` respectively. **Do NOT generate these files** -- they must be copied from the reference SDK at `OMX Mystique components/omx-component-sdk/omx-component-sdk/lib/`. If the reference SDK is not available locally, warn the user:
|
|
637
|
+
|
|
638
|
+
```
|
|
639
|
+
WARNING: lib/mystique.js and lib/material-ui.js are required but cannot be auto-generated.
|
|
640
|
+
These files must be obtained from the Fluent Commerce OMX Component SDK distribution.
|
|
641
|
+
Copy them from: OMX Mystique components/omx-component-sdk/omx-component-sdk/lib/
|
|
642
|
+
Without these files, Jest tests and Storybook will fail (missing global Mystique/MaterialUI objects).
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### src/index.tsx (Entry Point)
|
|
646
|
+
|
|
647
|
+
This is the entry point for the webpack bundle. All component registrations happen here.
|
|
648
|
+
|
|
649
|
+
```tsx
|
|
650
|
+
import { ComponentRegistry } from 'mystique/registry/ComponentRegistry';
|
|
651
|
+
// Import each component
|
|
652
|
+
import { ${ComponentName} } from './components/${ComponentName}/${ComponentName}';
|
|
653
|
+
|
|
654
|
+
// Register with Mystique -- aliases are how the manifest references this component
|
|
655
|
+
// Convention: <namespace>.<category>.<kebab-name>
|
|
656
|
+
ComponentRegistry.register(
|
|
657
|
+
['${NAMESPACE}.${CATEGORY}.${KEBAB_NAME}'],
|
|
658
|
+
${ComponentName},
|
|
659
|
+
{ category: '${CATEGORY}' }
|
|
660
|
+
);
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
**Alias naming convention:** `<namespace>.<category>.<component-name>` where:
|
|
664
|
+
- `namespace` is the company/project prefix (e.g., `acme`, `hm`, `curbside`)
|
|
665
|
+
- `category` is one of `page`, `layout`, `content`, `filter`
|
|
666
|
+
- `component-name` is kebab-case (e.g., `order-tracker`, `store-map`)
|
|
667
|
+
|
|
668
|
+
Multiple aliases can be registered for the same component for backward compatibility.
|
|
669
|
+
|
|
670
|
+
### src/index.html (Dev Server)
|
|
671
|
+
|
|
672
|
+
```html
|
|
673
|
+
<!DOCTYPE html>
|
|
674
|
+
<html lang="en">
|
|
675
|
+
<head>
|
|
676
|
+
<meta charset="UTF-8">
|
|
677
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
678
|
+
<title>${PROJECT_TITLE} - Mystique Plugin</title>
|
|
679
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
|
680
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
|
|
681
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
|
|
682
|
+
<style>
|
|
683
|
+
body { display:flex; flex-direction: column; align-items: center; justify-content: center; background-color:#efefef; font-family: 'Lato', sans-serif; }
|
|
684
|
+
#root { width: 600px; max-width:90%; padding: 4em 2em 2em 2em; }
|
|
685
|
+
</style>
|
|
686
|
+
</head>
|
|
687
|
+
<body>
|
|
688
|
+
<div id="root">
|
|
689
|
+
<p>Welcome to the ${PROJECT_TITLE} Mystique Plugin!</p>
|
|
690
|
+
<p>Add this plugin to your Fluent OMX app manifest:</p>
|
|
691
|
+
<pre>
|
|
692
|
+
{
|
|
693
|
+
"plugins": [
|
|
694
|
+
{ "type": "url", "src": "http://localhost:3001" }
|
|
695
|
+
]
|
|
696
|
+
}
|
|
697
|
+
</pre>
|
|
698
|
+
</div>
|
|
699
|
+
</body>
|
|
700
|
+
</html>
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### src/setup-tests.ts
|
|
704
|
+
|
|
705
|
+
```typescript
|
|
706
|
+
import { matchers } from '@emotion/jest';
|
|
707
|
+
|
|
708
|
+
expect.extend(matchers);
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### src/utils/TestUtils.tsx
|
|
712
|
+
|
|
713
|
+
Provides consistent Material-UI classname generation for snapshot tests.
|
|
714
|
+
|
|
715
|
+
```tsx
|
|
716
|
+
import { FC } from 'react';
|
|
717
|
+
import { StylesProvider, StylesOptions } from '@material-ui/styles';
|
|
718
|
+
|
|
719
|
+
const generateClassName: StylesOptions['generateClassName'] = (
|
|
720
|
+
rule,
|
|
721
|
+
sheet,
|
|
722
|
+
): string => `${sheet!.options.classNamePrefix}-${rule.key}`;
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Provides consistent classname generation for MaterialUI components to avoid inconsistent snapshots.
|
|
726
|
+
*/
|
|
727
|
+
export const FixMuiClassnames: FC = ({ children }) => {
|
|
728
|
+
return (
|
|
729
|
+
<StylesProvider generateClassName={generateClassName}>
|
|
730
|
+
{children}
|
|
731
|
+
</StylesProvider>
|
|
732
|
+
);
|
|
733
|
+
};
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
### src/utils/GqlUtils.ts
|
|
737
|
+
|
|
738
|
+
```typescript
|
|
739
|
+
import { DocumentNode } from 'graphql';
|
|
740
|
+
import { print } from 'graphql/language/printer';
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Converts a GraphQL document node back to a query string.
|
|
744
|
+
* Useful for defining queries with gql`` template literals in manifests
|
|
745
|
+
* and converting them to strings for the manifest JSON output.
|
|
746
|
+
*/
|
|
747
|
+
export const parse = (doc: DocumentNode) => print(doc);
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
## Component Template
|
|
751
|
+
|
|
752
|
+
For each component in `--components`, generate the following files:
|
|
753
|
+
|
|
754
|
+
### Component File (`src/components/<Name>/<Name>.tsx`)
|
|
755
|
+
|
|
756
|
+
```tsx
|
|
757
|
+
import React from 'react';
|
|
758
|
+
import { Card } from 'mystique/components/Card';
|
|
759
|
+
import { CardContent } from 'mystique/components/CardContent';
|
|
760
|
+
import { StdProps } from 'mystique/registry/ComponentRegistry';
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Props for the ${Name} component.
|
|
764
|
+
* Extends StdProps to receive injected data from the page query via dataSource.
|
|
765
|
+
*/
|
|
766
|
+
export interface ${Name}Props extends StdProps {
|
|
767
|
+
/**
|
|
768
|
+
* Component title displayed in the card header.
|
|
769
|
+
*/
|
|
770
|
+
title?: string;
|
|
771
|
+
// TODO: Define component-specific props
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* ${Name} -- a custom Mystique component.
|
|
776
|
+
*
|
|
777
|
+
* Usage in manifest:
|
|
778
|
+
* { "component": "${NAMESPACE}.${CATEGORY}.${KEBAB_NAME}", "props": { "title": "My Title" } }
|
|
779
|
+
*/
|
|
780
|
+
export const ${Name}: React.FC<${Name}Props> = ({ data, title = '${Name}', ...props }) => {
|
|
781
|
+
return (
|
|
782
|
+
<Card title={title}>
|
|
783
|
+
<CardContent>
|
|
784
|
+
<p data-testid="${KEBAB_NAME}-content">
|
|
785
|
+
{/* TODO: Implement component using data from props.data */}
|
|
786
|
+
{data ? JSON.stringify(data) : 'No data available'}
|
|
787
|
+
</p>
|
|
788
|
+
</CardContent>
|
|
789
|
+
</Card>
|
|
790
|
+
);
|
|
791
|
+
};
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
### Test File (`src/components/<Name>/<Name>.test.tsx`)
|
|
795
|
+
|
|
796
|
+
```tsx
|
|
797
|
+
import { render, screen } from '@testing-library/react';
|
|
798
|
+
import { ${Name} } from './${Name}';
|
|
799
|
+
|
|
800
|
+
describe('${Name}', () => {
|
|
801
|
+
it('renders without crashing', () => {
|
|
802
|
+
render(<${Name} />);
|
|
803
|
+
expect(screen.getByTestId('${KEBAB_NAME}-content')).toBeInTheDocument();
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it('displays title prop', () => {
|
|
807
|
+
const { container } = render(<${Name} title="Custom Title" />);
|
|
808
|
+
expect(container.textContent).toContain('Custom Title');
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it('renders data when provided', () => {
|
|
812
|
+
const mockData = { id: '123', ref: 'TEST-001' };
|
|
813
|
+
render(<${Name} data={mockData} />);
|
|
814
|
+
expect(screen.getByTestId('${KEBAB_NAME}-content').textContent).toContain('TEST-001');
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
### Story File (`src/components/<Name>/<Name>.stories.tsx`)
|
|
820
|
+
|
|
821
|
+
```tsx
|
|
822
|
+
import { ComponentMeta } from '@storybook/react';
|
|
823
|
+
import { storyTemplate } from '../../../.storybook/helpers';
|
|
824
|
+
import { ${Name} } from './${Name}';
|
|
825
|
+
|
|
826
|
+
export default {
|
|
827
|
+
title: 'Components/${Name}',
|
|
828
|
+
component: ${Name},
|
|
829
|
+
} as ComponentMeta<typeof ${Name}>;
|
|
830
|
+
|
|
831
|
+
const template = storyTemplate(${Name});
|
|
832
|
+
|
|
833
|
+
export const Default = template({
|
|
834
|
+
title: '${Name} Component',
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
export const WithData = template({
|
|
838
|
+
title: '${Name} with Data',
|
|
839
|
+
data: {
|
|
840
|
+
id: '1',
|
|
841
|
+
ref: 'EXAMPLE-001',
|
|
842
|
+
status: 'ACTIVE',
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
### Index File (`src/components/<Name>/index.ts`)
|
|
848
|
+
|
|
849
|
+
```typescript
|
|
850
|
+
export { ${Name} } from './${Name}';
|
|
851
|
+
export type { ${Name}Props } from './${Name}';
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
## Storybook Configuration
|
|
855
|
+
|
|
856
|
+
### .storybook/main.ts
|
|
857
|
+
|
|
858
|
+
```typescript
|
|
859
|
+
import { StorybookConfig } from '@storybook/core-common';
|
|
860
|
+
import { externalMaterialUI, externalMystique } from '../webpack.config';
|
|
861
|
+
|
|
862
|
+
const config: StorybookConfig = {
|
|
863
|
+
webpackFinal: async (config: any) => {
|
|
864
|
+
config.externals = [{
|
|
865
|
+
'react': 'React',
|
|
866
|
+
'react-dom': 'ReactDOM',
|
|
867
|
+
},
|
|
868
|
+
externalMaterialUI,
|
|
869
|
+
externalMystique
|
|
870
|
+
];
|
|
871
|
+
return config;
|
|
872
|
+
},
|
|
873
|
+
stories: [
|
|
874
|
+
'../src/**/*.stories.mdx',
|
|
875
|
+
'../src/**/*.stories.@(ts|tsx)'
|
|
876
|
+
],
|
|
877
|
+
addons: [
|
|
878
|
+
'@storybook/addon-links',
|
|
879
|
+
'@storybook/addon-essentials'
|
|
880
|
+
],
|
|
881
|
+
core: {
|
|
882
|
+
builder: 'webpack5'
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
module.exports = config;
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
### .storybook/preview.js
|
|
890
|
+
|
|
891
|
+
```javascript
|
|
892
|
+
import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
|
|
893
|
+
import { FixMuiClassnames } from '../src/utils/TestUtils';
|
|
894
|
+
|
|
895
|
+
export const parameters = {
|
|
896
|
+
actions: { argTypesRegex: "^on[A-Z].*" },
|
|
897
|
+
viewport: {
|
|
898
|
+
defaultViewport: 'iphone5',
|
|
899
|
+
viewports: INITIAL_VIEWPORTS
|
|
900
|
+
},
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
export const decorators = [
|
|
904
|
+
(Story) => (
|
|
905
|
+
<FixMuiClassnames>
|
|
906
|
+
<Story />
|
|
907
|
+
</FixMuiClassnames>
|
|
908
|
+
)
|
|
909
|
+
];
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
### .storybook/helpers.ts
|
|
913
|
+
|
|
914
|
+
```typescript
|
|
915
|
+
import { ComponentStory, Story } from '@storybook/react';
|
|
916
|
+
|
|
917
|
+
export const storyTemplate = <P,>(Component: (props: P) => any) => (
|
|
918
|
+
props: P
|
|
919
|
+
): Story<P> => {
|
|
920
|
+
const template: ComponentStory<typeof Component> = (args) => Component(args);
|
|
921
|
+
const story = template.bind({});
|
|
922
|
+
story.args = props;
|
|
923
|
+
return story;
|
|
924
|
+
};
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
## @types/mystique/ Type Definitions
|
|
928
|
+
|
|
929
|
+
The `@types/mystique/` directory contains 7 TypeScript declaration files that define the Mystique SDK API surface. These must be copied from the reference SDK or generated from the templates below.
|
|
930
|
+
|
|
931
|
+
| File | Purpose |
|
|
932
|
+
|------|---------|
|
|
933
|
+
| `index.d.ts` | Common types: `TranslatableString`, `QueryResponse<T>`, `Connection<T>`, `Edge<T>` |
|
|
934
|
+
| `manifest.d.ts` | Full manifest v2.0 schema: `MystiqueManifest`, `MystiqueSection`, `MystiquePage`, `MystiqueComponentInstance`, `MystiquePlugin`, `MystiqueManifestFragment`, `GQLQuery` |
|
|
935
|
+
| `components.d.ts` | Component prop interfaces: `Card`, `CardContent`, `Children`, `DynamicValue`, `Loading`, `List`, `QuantityList`, `QuantitySelectorComponent`, `DatePicker` |
|
|
936
|
+
| `hooks.d.ts` | Hook definitions: `useAuth`, `useEnv`, `useI18n`, `useSettings`, `useQuery`, `getQuery`, `useRest`, `getRest`, `useData`, `useUserActions`, `useUserActionForm`, `getApiDownload`, `getSettings` |
|
|
937
|
+
| `services.d.ts` | Service types: `MystiqueThemeColours` (color palette interface for theming) |
|
|
938
|
+
| `registry.d.ts` | Registry interfaces: `ComponentRegistry` (register/get components), `FieldRegistry` (register/get form fields), `TemplateRegistry` (register/render Handlebars helpers) |
|
|
939
|
+
| `frame.d.ts` | Frame utilities: `pushModal`, `pushDrawer`, `pushToast`, `DataModal`, `clearModals`, `clearDrawers` |
|
|
940
|
+
|
|
941
|
+
**These type definitions are the contract between your component code and the Mystique host application.** They provide full IntelliSense in the IDE for all Mystique hooks, components, and registries. The actual implementations are provided at runtime by the host app via `window.Mystique`.
|
|
942
|
+
|
|
943
|
+
## React Hooks Usage Patterns
|
|
944
|
+
|
|
945
|
+
The Mystique SDK provides hooks via `import { useAuth, useQuery, ... } from 'mystique/hooks'`. All hooks are provided by the host app at runtime.
|
|
946
|
+
|
|
947
|
+
### useAuth — Authentication and Context
|
|
948
|
+
|
|
949
|
+
```typescript
|
|
950
|
+
import { useAuth } from 'mystique/hooks';
|
|
951
|
+
|
|
952
|
+
const MyComponent = () => {
|
|
953
|
+
const auth = useAuth();
|
|
954
|
+
// auth.username: string — current user's login name
|
|
955
|
+
// auth.firstName, auth.lastName: string
|
|
956
|
+
// auth.timezone: string — user's timezone (e.g., 'Australia/Sydney')
|
|
957
|
+
// auth.language: string — user's locale (e.g., 'en')
|
|
958
|
+
// auth.roles: Array<{ name: string }> — assigned roles
|
|
959
|
+
// auth.contextId: number — current retailer/location context ID
|
|
960
|
+
// auth.contextType: string — 'RETAILER' | 'LOCATION'
|
|
961
|
+
// auth.switchContext(contextId, contextType) — switch retailer/location
|
|
962
|
+
// auth.logout() — end session
|
|
963
|
+
|
|
964
|
+
const isAdmin = auth.roles.some(r => r.name === 'ADMIN');
|
|
965
|
+
return isAdmin ? <AdminPanel /> : <StandardView />;
|
|
966
|
+
};
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
### useQuery — GraphQL Data Fetching
|
|
970
|
+
|
|
971
|
+
```typescript
|
|
972
|
+
import { useQuery } from 'mystique/hooks';
|
|
973
|
+
|
|
974
|
+
const OrderDetail = ({ orderId }: { orderId: string }) => {
|
|
975
|
+
const [result, { loading, error, refetch }] = useQuery(
|
|
976
|
+
`query OrderById($id: ID!) { orderById(id: $id) { id ref status type createdOn } }`,
|
|
977
|
+
{ id: orderId }
|
|
978
|
+
);
|
|
979
|
+
|
|
980
|
+
if (loading) return <Loading />;
|
|
981
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
982
|
+
|
|
983
|
+
const order = result?.orderById;
|
|
984
|
+
// Connection/pagination pattern:
|
|
985
|
+
// result.orders.edges[].node — array of entities
|
|
986
|
+
// result.orders.pageInfo.hasNextPage — boolean
|
|
987
|
+
// result.orders.pageInfo.endCursor — cursor for next page
|
|
988
|
+
return <div>{order.ref} — {order.status}</div>;
|
|
989
|
+
};
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
### useSettings — Hierarchical Settings Access
|
|
993
|
+
|
|
994
|
+
```typescript
|
|
995
|
+
import { useSettings } from 'mystique/hooks';
|
|
996
|
+
|
|
997
|
+
const ConfiguredComponent = () => {
|
|
998
|
+
const settings = useSettings(['fc.myFeature.enabled', 'fc.myFeature.maxItems']);
|
|
999
|
+
// Settings cascade: Location > Retailer > Account > Global (most specific wins)
|
|
1000
|
+
// Each key returns: { status: 'loading' | 'error' | 'success', result?: { value: string } }
|
|
1001
|
+
|
|
1002
|
+
const featureEnabled = settings['fc.myFeature.enabled'];
|
|
1003
|
+
if (featureEnabled.status === 'loading') return <Loading />;
|
|
1004
|
+
const enabled = featureEnabled.result?.value === 'true';
|
|
1005
|
+
return enabled ? <FeatureUI /> : null;
|
|
1006
|
+
};
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
### useI18n — Internationalization
|
|
1010
|
+
|
|
1011
|
+
```typescript
|
|
1012
|
+
import { useI18n } from 'mystique/hooks';
|
|
1013
|
+
|
|
1014
|
+
const TranslatedComponent = () => {
|
|
1015
|
+
const { translate, translateOr } = useI18n();
|
|
1016
|
+
|
|
1017
|
+
// translate(key) — single key lookup, returns key if not found
|
|
1018
|
+
const title = translate('fc.om.orders.title');
|
|
1019
|
+
|
|
1020
|
+
// translateOr([key1, key2, ..., fallback]) — try keys in order, last is fallback
|
|
1021
|
+
const label = translateOr(['fc.custom.order.label', 'fc.om.orders.label', 'Orders']);
|
|
1022
|
+
|
|
1023
|
+
return <h1>{title}</h1>;
|
|
1024
|
+
};
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
### useData — Page Query Data Access
|
|
1028
|
+
|
|
1029
|
+
```typescript
|
|
1030
|
+
import { useData } from 'mystique/hooks';
|
|
1031
|
+
|
|
1032
|
+
const PageComponent = () => {
|
|
1033
|
+
const data = useData();
|
|
1034
|
+
// data — contains the response from the page-level GraphQL query
|
|
1035
|
+
// In a list page: data.orders.edges, data.orders.pageInfo
|
|
1036
|
+
// In a detail page: data.orderById.ref, data.orderById.status
|
|
1037
|
+
// Variables and filters are managed by the host app
|
|
1038
|
+
return <span>{data?.orderById?.ref}</span>;
|
|
1039
|
+
};
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
### useEnv — Environment Details
|
|
1043
|
+
|
|
1044
|
+
```typescript
|
|
1045
|
+
import { useEnv } from 'mystique/hooks';
|
|
1046
|
+
|
|
1047
|
+
const DebugInfo = () => {
|
|
1048
|
+
const env = useEnv();
|
|
1049
|
+
// env.accountName: string — Fluent account name
|
|
1050
|
+
// env.appName: string — 'oms', 'store', 'servicepoint'
|
|
1051
|
+
// env.apiEndpoint: string — API base URL
|
|
1052
|
+
return <span>Connected to {env.accountName}</span>;
|
|
1053
|
+
};
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
## Component Composition Primitives
|
|
1057
|
+
|
|
1058
|
+
Three essential patterns for building reusable, manifest-configurable custom components.
|
|
1059
|
+
|
|
1060
|
+
### DynamicValue — Manifest-Configurable Content
|
|
1061
|
+
|
|
1062
|
+
`DynamicValue` renders a single dynamic attribute (text, image, or component) from manifest config. Lets manifest configurers control what your component displays without code changes.
|
|
1063
|
+
|
|
1064
|
+
```typescript
|
|
1065
|
+
import { DynamicValue } from 'mystique/components';
|
|
1066
|
+
|
|
1067
|
+
const OrderCard = ({ data, props }) => {
|
|
1068
|
+
// props.attributes comes from the manifest's attributes[] array
|
|
1069
|
+
return (
|
|
1070
|
+
<div>
|
|
1071
|
+
{props.attributes?.map((attr, i) => (
|
|
1072
|
+
<div key={i}>
|
|
1073
|
+
<label>{attr.label}</label>
|
|
1074
|
+
<DynamicValue attribute={attr} context={data} defaultValue={<span>-</span>} />
|
|
1075
|
+
</div>
|
|
1076
|
+
))}
|
|
1077
|
+
</div>
|
|
1078
|
+
);
|
|
1079
|
+
};
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
### Children — Manifest-Driven Composition
|
|
1083
|
+
|
|
1084
|
+
`Children` renders descendant components configured in the manifest. Lets configurers place OOTB or custom components inside your component via `descendants`.
|
|
1085
|
+
|
|
1086
|
+
```typescript
|
|
1087
|
+
import { Children } from 'mystique/components';
|
|
1088
|
+
|
|
1089
|
+
const Panel = ({ data, children }) => {
|
|
1090
|
+
return (
|
|
1091
|
+
<div className="custom-panel">
|
|
1092
|
+
<Children /> {/* renders all descendants from manifest */}
|
|
1093
|
+
</div>
|
|
1094
|
+
);
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
// Wrapper variant: transform data for all descendants
|
|
1098
|
+
const FilteredPanel = ({ data }) => {
|
|
1099
|
+
const filteredData = data.items.filter(i => i.status === 'ACTIVE');
|
|
1100
|
+
return (
|
|
1101
|
+
<div>
|
|
1102
|
+
<Children dataOverride={filteredData} />
|
|
1103
|
+
</div>
|
|
1104
|
+
);
|
|
1105
|
+
};
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
### TemplateRegistry.render — Resolve Templates in Code
|
|
1109
|
+
|
|
1110
|
+
Use `TemplateRegistry.render()` to resolve Handlebars template strings inside component code.
|
|
1111
|
+
|
|
1112
|
+
```typescript
|
|
1113
|
+
import { TemplateRegistry } from 'mystique/registry';
|
|
1114
|
+
|
|
1115
|
+
const CustomDisplay = ({ data, props }) => {
|
|
1116
|
+
// props.titleTemplate might be "Order {{ref}} — {{status}}" from manifest
|
|
1117
|
+
const resolvedTitle = TemplateRegistry.render(props.titleTemplate, data);
|
|
1118
|
+
return <h2>{resolvedTitle}</h2>;
|
|
1119
|
+
};
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
### FieldRegistry.get — Reuse OOTB Form Fields
|
|
1123
|
+
|
|
1124
|
+
Compose existing OOTB field components inside custom composite fields:
|
|
1125
|
+
|
|
1126
|
+
```typescript
|
|
1127
|
+
import { FieldRegistry } from 'mystique/registry';
|
|
1128
|
+
|
|
1129
|
+
const CompositeField = ({ onChange, value }) => {
|
|
1130
|
+
const NumberField = FieldRegistry.get('number');
|
|
1131
|
+
return (
|
|
1132
|
+
<div>
|
|
1133
|
+
<NumberField value={value.min} onChange={(v) => onChange({ ...value, min: v })} />
|
|
1134
|
+
<span> to </span>
|
|
1135
|
+
<NumberField value={value.max} onChange={(v) => onChange({ ...value, max: v })} />
|
|
1136
|
+
</div>
|
|
1137
|
+
);
|
|
1138
|
+
};
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
### Form Field onChange — Avoid Infinite Loop
|
|
1142
|
+
|
|
1143
|
+
**CRITICAL:** When building form field components, `onChange` triggers a re-render. Never call `onChange` in the render body — wrap in `useEffect`:
|
|
1144
|
+
|
|
1145
|
+
```typescript
|
|
1146
|
+
// WRONG — infinite re-render loop
|
|
1147
|
+
const BadField = ({ onChange, value }) => {
|
|
1148
|
+
onChange(value || 'default'); // called every render!
|
|
1149
|
+
return <input value={value} />;
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
// CORRECT — useEffect prevents the loop
|
|
1153
|
+
const GoodField = ({ onChange, value }) => {
|
|
1154
|
+
useEffect(() => {
|
|
1155
|
+
if (!value) onChange('default');
|
|
1156
|
+
}, [value]); // only runs when value changes
|
|
1157
|
+
|
|
1158
|
+
return <input value={value} onChange={(e) => onChange(e.target.value)} />;
|
|
1159
|
+
};
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
### User Action Attribute to Field Mapping
|
|
1163
|
+
|
|
1164
|
+
Workflow user actions define `eventAttributes` with a `type` property. The Mystique form engine maps each type to a FieldRegistry entry:
|
|
1165
|
+
|
|
1166
|
+
| Workflow `eventAttributes[].type` | FieldRegistry Entry | Input Type |
|
|
1167
|
+
|---|---|---|
|
|
1168
|
+
| `TEXT` / `STRING` | text | Single-line text input |
|
|
1169
|
+
| `TEXTAREA` | textarea | Multi-line text area |
|
|
1170
|
+
| `NUMBER` / `INTEGER` | number | Numeric input |
|
|
1171
|
+
| `BOOLEAN` | boolean | Toggle switch |
|
|
1172
|
+
| `DATE` | date | Date picker |
|
|
1173
|
+
| `DROPDOWN` | dropdown | Select from options |
|
|
1174
|
+
| Custom type name | Custom field (if registered) | Your custom component |
|
|
1175
|
+
|
|
1176
|
+
Register custom fields with `FieldRegistry.register('myCustomType', MyFieldComponent)`. The form engine auto-selects them when a user action has `type: "myCustomType"`.
|
|
1177
|
+
|
|
1178
|
+
## Build & Development Lifecycle
|
|
1179
|
+
|
|
1180
|
+
### Development Cycle
|
|
1181
|
+
|
|
1182
|
+
```
|
|
1183
|
+
1. Scaffold project
|
|
1184
|
+
/fluent-mystique-scaffold my-plugin --components OrderTracker,StoreMap
|
|
1185
|
+
|
|
1186
|
+
2. Install dependencies
|
|
1187
|
+
cd accounts/<PROFILE>/SOURCE/my-plugin/
|
|
1188
|
+
yarn install
|
|
1189
|
+
|
|
1190
|
+
3. Start dev server
|
|
1191
|
+
yarn start
|
|
1192
|
+
-> Serves on http://localhost:3001
|
|
1193
|
+
-> Outputs bundle.[hash].js with CORS headers
|
|
1194
|
+
|
|
1195
|
+
4. Point manifest plugins[] to localhost
|
|
1196
|
+
In the Fluent OMX app manifest (via Settings or /fluent-mystique-builder):
|
|
1197
|
+
{
|
|
1198
|
+
"plugins": [
|
|
1199
|
+
{ "type": "url", "src": "http://localhost:3001/" }
|
|
1200
|
+
]
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
5. Test in Fluent OMX App
|
|
1204
|
+
-> Load the OMX app in browser
|
|
1205
|
+
-> The host app fetches version.json from localhost:3001
|
|
1206
|
+
-> Loads the hashed bundle
|
|
1207
|
+
-> Your components are now available in the manifest
|
|
1208
|
+
|
|
1209
|
+
6. Write tests
|
|
1210
|
+
yarn test # Run Jest with coverage
|
|
1211
|
+
yarn storybook # Visual component testing on port 6006
|
|
1212
|
+
|
|
1213
|
+
7. Iterate
|
|
1214
|
+
-> Webpack HMR reloads on save
|
|
1215
|
+
-> Refresh the OMX app to pick up changes
|
|
1216
|
+
```
|
|
1217
|
+
|
|
1218
|
+
### Production Deployment
|
|
1219
|
+
|
|
1220
|
+
```
|
|
1221
|
+
1. Build the bundle
|
|
1222
|
+
yarn build
|
|
1223
|
+
-> Produces dist/bundle.[contenthash].js
|
|
1224
|
+
-> Produces dist/version.json (maps bundle filename)
|
|
1225
|
+
-> Produces dist/manifests/*.json (compiled manifest examples)
|
|
1226
|
+
-> Runs build tests to verify output
|
|
1227
|
+
|
|
1228
|
+
2. Deploy to hosting
|
|
1229
|
+
Fluent Commerce does NOT host custom component bundles.
|
|
1230
|
+
The bundle must be self-hosted. Options:
|
|
1231
|
+
|
|
1232
|
+
a) Vercel (recommended for simplicity):
|
|
1233
|
+
vercel deploy
|
|
1234
|
+
-> Returns URL like https://my-plugin.vercel.app/
|
|
1235
|
+
|
|
1236
|
+
b) AWS S3 + CloudFront:
|
|
1237
|
+
aws s3 sync dist/ s3://my-bucket/my-plugin/ --cache-control max-age=31536000
|
|
1238
|
+
|
|
1239
|
+
c) Any static hosting / CDN:
|
|
1240
|
+
Upload dist/ contents to your hosting provider
|
|
1241
|
+
|
|
1242
|
+
3. Update manifest plugins[] with production URL
|
|
1243
|
+
{
|
|
1244
|
+
"plugins": [
|
|
1245
|
+
{
|
|
1246
|
+
"type": "url",
|
|
1247
|
+
"src": "https://my-plugin.vercel.app/",
|
|
1248
|
+
"name": "my-plugin"
|
|
1249
|
+
}
|
|
1250
|
+
]
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
NOTE: The trailing "/" in the src URL enables version.json auto-discovery.
|
|
1254
|
+
The host app fetches <src>/version.json, reads the bundle filename,
|
|
1255
|
+
then loads <src>/<bundle-filename>.
|
|
1256
|
+
|
|
1257
|
+
4. Deploy manifest to Fluent environment
|
|
1258
|
+
Use /fluent-mystique-builder to update the manifest, then
|
|
1259
|
+
/fluent-settings to upload it as a Fluent Setting.
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1262
|
+
### Key Deployment Facts
|
|
1263
|
+
|
|
1264
|
+
- **Fluent Commerce does NOT host custom component bundles.** The bundle must be self-hosted on Vercel, S3, CDN, or any static file server.
|
|
1265
|
+
- **The manifest `plugins[]` array loads bundles by URL at runtime.** The OMX host app fetches each plugin URL, executes the JavaScript, and the `ComponentRegistry.register()` calls make components available.
|
|
1266
|
+
- **Webpack externals ensure React, ReactDOM, and Material-UI are NOT bundled.** These are provided by the host app. Including them would cause version conflicts and dramatically increase bundle size.
|
|
1267
|
+
- **Bundle output uses `[contenthash]` for cache busting.** The `version.json` file maps the current hash, allowing the host app to discover the latest bundle without hardcoding the hash in the manifest.
|
|
1268
|
+
- **For local development, webpack dev server serves on port 3001** with CORS headers enabled so the host app on a different domain can load the bundle.
|
|
1269
|
+
|
|
1270
|
+
### Vercel Deployment (Optional Convenience)
|
|
1271
|
+
|
|
1272
|
+
If using Vercel, generate a `vercel.json` in the project root:
|
|
1273
|
+
|
|
1274
|
+
```json
|
|
1275
|
+
{
|
|
1276
|
+
"version": 2,
|
|
1277
|
+
"builds": [
|
|
1278
|
+
{
|
|
1279
|
+
"src": "dist/**",
|
|
1280
|
+
"use": "@vercel/static"
|
|
1281
|
+
}
|
|
1282
|
+
],
|
|
1283
|
+
"routes": [
|
|
1284
|
+
{
|
|
1285
|
+
"src": "/(.*)",
|
|
1286
|
+
"dest": "/dist/$1"
|
|
1287
|
+
}
|
|
1288
|
+
]
|
|
1289
|
+
}
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
Deploy with:
|
|
1293
|
+
```bash
|
|
1294
|
+
yarn build
|
|
1295
|
+
vercel deploy --prod
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
The production URL is stable and can be used directly in the manifest `plugins[]` array.
|
|
1299
|
+
|
|
1300
|
+
## Build Plugins
|
|
1301
|
+
|
|
1302
|
+
### build/GenerateVersionPlugin.ts
|
|
1303
|
+
|
|
1304
|
+
Emits `version.json` alongside the bundle, mapping the hashed filename:
|
|
1305
|
+
|
|
1306
|
+
```typescript
|
|
1307
|
+
import { Compiler, sources } from 'webpack';
|
|
1308
|
+
|
|
1309
|
+
class GenerateVersionPlugin {
|
|
1310
|
+
apply(compiler: Compiler) {
|
|
1311
|
+
compiler.hooks.afterCompile.tap('GenerateVersionPlugin', (compilation) => {
|
|
1312
|
+
Object.entries(compilation.assets).forEach(([pathname]) => {
|
|
1313
|
+
if (pathname.match(/bundle\.[a-z0-9]+\.js$/))
|
|
1314
|
+
compilation.assets['version.json'] =
|
|
1315
|
+
new sources.RawSource(JSON.stringify({
|
|
1316
|
+
bundle: pathname,
|
|
1317
|
+
version: process.env.GIT_TAG || Date.now().toString()
|
|
1318
|
+
}));
|
|
1319
|
+
});
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
module.exports = GenerateVersionPlugin;
|
|
1325
|
+
```
|
|
1326
|
+
|
|
1327
|
+
### build/GenerateManifestsPlugin.ts
|
|
1328
|
+
|
|
1329
|
+
Compiles TypeScript manifest files in `manifests/` to JSON in `dist/manifests/`:
|
|
1330
|
+
|
|
1331
|
+
```typescript
|
|
1332
|
+
import webpack from 'webpack';
|
|
1333
|
+
const fs = require('fs-extra');
|
|
1334
|
+
|
|
1335
|
+
class GenerateManifestsPlugin {
|
|
1336
|
+
apply(compiler: webpack.Compiler) {
|
|
1337
|
+
compiler.hooks.afterEmit.tap('GenerateManifestsPlugin', () => {
|
|
1338
|
+
fs.readdir('./manifests', (err: string, files: string[]) => {
|
|
1339
|
+
if (err) {
|
|
1340
|
+
console.log(err);
|
|
1341
|
+
} else {
|
|
1342
|
+
files.forEach((file: string) => {
|
|
1343
|
+
if (file.includes('fc.mystique.manifest') && !file.includes('.test.')) {
|
|
1344
|
+
const filenameNoExt = file.slice(0, -3);
|
|
1345
|
+
import(`../manifests/${filenameNoExt}`).then(manifest => {
|
|
1346
|
+
fs.outputFile(
|
|
1347
|
+
`dist/manifests/${filenameNoExt}.json`,
|
|
1348
|
+
JSON.stringify(manifest.manifest, null, 4)
|
|
1349
|
+
);
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
module.exports = GenerateManifestsPlugin;
|
|
1360
|
+
```
|
|
1361
|
+
|
|
1362
|
+
## .gitignore
|
|
1363
|
+
|
|
1364
|
+
```gitignore
|
|
1365
|
+
# Dependencies
|
|
1366
|
+
node_modules/
|
|
1367
|
+
|
|
1368
|
+
# Build output
|
|
1369
|
+
dist/
|
|
1370
|
+
|
|
1371
|
+
# Test output
|
|
1372
|
+
test-report/
|
|
1373
|
+
coverage/
|
|
1374
|
+
|
|
1375
|
+
# IDE files
|
|
1376
|
+
.idea/
|
|
1377
|
+
*.iml
|
|
1378
|
+
.vscode/
|
|
1379
|
+
|
|
1380
|
+
# OS files
|
|
1381
|
+
.DS_Store
|
|
1382
|
+
Thumbs.db
|
|
1383
|
+
|
|
1384
|
+
# Logs
|
|
1385
|
+
*.log
|
|
1386
|
+
yarn-debug.log*
|
|
1387
|
+
yarn-error.log*
|
|
1388
|
+
|
|
1389
|
+
# Environment
|
|
1390
|
+
.env
|
|
1391
|
+
.env.local
|
|
1392
|
+
.env.production
|
|
1393
|
+
|
|
1394
|
+
# Storybook
|
|
1395
|
+
storybook-static/
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
## Execution Flow
|
|
1399
|
+
|
|
1400
|
+
```
|
|
1401
|
+
1. VALIDATE inputs
|
|
1402
|
+
a. project-name: must match /^[a-z][a-z0-9-]*$/ (lowercase, hyphens allowed)
|
|
1403
|
+
b. component names (if --components): must match /^[A-Z][a-zA-Z0-9]*$/ (PascalCase)
|
|
1404
|
+
c. category (if --category): must be page|layout|content|filter
|
|
1405
|
+
|
|
1406
|
+
2. PRE-CHECK: Project already exists?
|
|
1407
|
+
a. Check accounts/<PROFILE>/SOURCE/<project-name>/ exists
|
|
1408
|
+
b. If exists -> ABORT with guidance (or add components if --components specified)
|
|
1409
|
+
|
|
1410
|
+
3. DETECT namespace
|
|
1411
|
+
a. If --namespace provided -> use it
|
|
1412
|
+
b. Else derive from project-name
|
|
1413
|
+
c. Fallback: "custom" with warning
|
|
1414
|
+
|
|
1415
|
+
4. COMPUTE derived values
|
|
1416
|
+
a. PROJECT_TITLE = title-case of project-name with hyphens as spaces
|
|
1417
|
+
b. KEBAB_NAME = kebab-case of each component name
|
|
1418
|
+
c. CATEGORY = from --category or default "content"
|
|
1419
|
+
d. Alias = <namespace>.<category>.<kebab-name>
|
|
1420
|
+
|
|
1421
|
+
5. CREATE directory structure
|
|
1422
|
+
a. Create all directories listed in the structure section
|
|
1423
|
+
b. Ensure all parent directories exist before writing files
|
|
1424
|
+
|
|
1425
|
+
6. COPY @types/mystique/ from reference SDK
|
|
1426
|
+
a. Search for reference SDK: OMX Mystique components/omx-component-sdk/omx-component-sdk/@types/mystique/
|
|
1427
|
+
b. Copy all 7 .d.ts files
|
|
1428
|
+
c. Also copy @types/images.d.ts
|
|
1429
|
+
|
|
1430
|
+
7. COPY lib/ files from reference SDK
|
|
1431
|
+
a. Copy mystique.js and material-ui.js (large bundled files, cannot be generated)
|
|
1432
|
+
b. Generate init.js, mystique-export.js from templates
|
|
1433
|
+
|
|
1434
|
+
8. GENERATE project config files
|
|
1435
|
+
a. package.json with substituted project name and description
|
|
1436
|
+
b. webpack.config.ts
|
|
1437
|
+
c. tsconfig.json
|
|
1438
|
+
d. jest.config.js
|
|
1439
|
+
e. .babelrc
|
|
1440
|
+
f. .gitignore
|
|
1441
|
+
g. sdk-version.json
|
|
1442
|
+
h. .storybook/main.ts, preview.js, helpers.ts
|
|
1443
|
+
|
|
1444
|
+
9. GENERATE source files
|
|
1445
|
+
a. src/index.tsx with ComponentRegistry.register() for each component
|
|
1446
|
+
b. src/index.html with project title
|
|
1447
|
+
c. src/setup-tests.ts
|
|
1448
|
+
d. src/utils/GqlUtils.ts, TestUtils.tsx
|
|
1449
|
+
e. manifests/fc.mystique.manifest.example.ts
|
|
1450
|
+
f. manifests/fc.mystique.manifest.fragment.example.ts
|
|
1451
|
+
g. build/GenerateVersionPlugin.ts, GenerateManifestsPlugin.ts
|
|
1452
|
+
h. i18n/locales/en/translation.json (empty object {})
|
|
1453
|
+
|
|
1454
|
+
10. FOR EACH component in --components (if specified):
|
|
1455
|
+
a. Create src/components/<Name>/ directory
|
|
1456
|
+
b. Generate <Name>.tsx using component template
|
|
1457
|
+
c. Generate <Name>.test.tsx using test template
|
|
1458
|
+
d. Generate <Name>.stories.tsx using story template
|
|
1459
|
+
e. Generate index.ts re-export
|
|
1460
|
+
f. Add import and ComponentRegistry.register() to src/index.tsx
|
|
1461
|
+
|
|
1462
|
+
11. VERIFY structure
|
|
1463
|
+
If yarn is available: yarn install && yarn check-types
|
|
1464
|
+
This confirms TypeScript compiles and dependencies resolve.
|
|
1465
|
+
|
|
1466
|
+
12. REPORT generated files
|
|
1467
|
+
List all files created with their full paths.
|
|
1468
|
+
Print next steps:
|
|
1469
|
+
- "Project scaffolded at: accounts/<PROFILE>/SOURCE/<project-name>/"
|
|
1470
|
+
- "Install dependencies: cd accounts/<PROFILE>/SOURCE/<project-name> && yarn install"
|
|
1471
|
+
- "Start dev server: yarn start (serves on http://localhost:3001)"
|
|
1472
|
+
- "Run tests: yarn test"
|
|
1473
|
+
- "Run Storybook: yarn storybook (serves on http://localhost:6006)"
|
|
1474
|
+
- "Build for production: yarn build (outputs to dist/)"
|
|
1475
|
+
- "Next: Wire components into a manifest with /fluent-mystique-builder"
|
|
1476
|
+
```
|
|
1477
|
+
|
|
1478
|
+
## Integration with Other Skills
|
|
1479
|
+
|
|
1480
|
+
| Skill | Relationship |
|
|
1481
|
+
|-------|-------------|
|
|
1482
|
+
| `/fluent-mystique-builder` | After scaffolding, use this to create or edit manifests that reference the new components via their registered aliases. |
|
|
1483
|
+
| `/fluent-mystique-validate` | Validate manifests that reference the new components. Checks that component aliases are known and props are well-formed. |
|
|
1484
|
+
| `/fluent-mystique-analyze` | Analyze deployed manifests to understand existing component usage before adding new ones. |
|
|
1485
|
+
| `/fluent-settings` | Deploy the compiled manifest JSON as a Fluent Setting for production use. |
|
|
1486
|
+
| `/fluent-feature-plan` | Required for multi-artifact work (SDK + manifest + workflows). Produces the comprehensive plan. |
|
|
1487
|
+
|
|
1488
|
+
### Post-Scaffold Workflow
|
|
1489
|
+
|
|
1490
|
+
```
|
|
1491
|
+
/fluent-mystique-scaffold mystique-plugin-curbside --components CurbsideStatus,StoreMap --category content
|
|
1492
|
+
|
|
|
1493
|
+
v
|
|
1494
|
+
cd accounts/<PROFILE>/SOURCE/mystique-plugin-curbside/ && yarn install
|
|
1495
|
+
|
|
|
1496
|
+
v
|
|
1497
|
+
yarn start (develop and test locally on port 3001)
|
|
1498
|
+
|
|
|
1499
|
+
v
|
|
1500
|
+
/fluent-mystique-builder (create manifest pages using curbside.content.curbside-status, curbside.content.store-map)
|
|
1501
|
+
|
|
|
1502
|
+
v
|
|
1503
|
+
/fluent-mystique-validate (validate the manifest before deployment)
|
|
1504
|
+
|
|
|
1505
|
+
v
|
|
1506
|
+
yarn build (produce dist/bundle.[hash].js)
|
|
1507
|
+
|
|
|
1508
|
+
v
|
|
1509
|
+
vercel deploy --prod (deploy bundle to hosting)
|
|
1510
|
+
|
|
|
1511
|
+
v
|
|
1512
|
+
/fluent-settings (upload manifest JSON to Fluent environment as a Setting)
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
## Edge Cases
|
|
1516
|
+
|
|
1517
|
+
### No components specified
|
|
1518
|
+
|
|
1519
|
+
When `--components` is omitted:
|
|
1520
|
+
- `src/index.tsx` contains only a comment: `// Register your components here`
|
|
1521
|
+
- No component directories are created
|
|
1522
|
+
- The project is still buildable (produces a bundle with no registrations)
|
|
1523
|
+
- This is valid as a starting point -- add components incrementally
|
|
1524
|
+
|
|
1525
|
+
### Project name conflicts
|
|
1526
|
+
|
|
1527
|
+
Check for any directory matching the project name:
|
|
1528
|
+
|
|
1529
|
+
```bash
|
|
1530
|
+
ls accounts/<PROFILE>/SOURCE/ | grep -i "<project-name>"
|
|
1531
|
+
```
|
|
1532
|
+
|
|
1533
|
+
If found, abort. Do NOT overwrite an existing project.
|
|
1534
|
+
|
|
1535
|
+
### Reference SDK not available
|
|
1536
|
+
|
|
1537
|
+
If `OMX Mystique components/omx-component-sdk/` is not found in the workspace:
|
|
1538
|
+
- Generate `@types/mystique/` files from the embedded templates (all 7 files documented in the plan)
|
|
1539
|
+
- Warn about missing `lib/mystique.js` and `lib/material-ui.js`
|
|
1540
|
+
- The project will build but tests and Storybook will fail without these lib files
|
|
1541
|
+
|
|
1542
|
+
### Windows path handling
|
|
1543
|
+
|
|
1544
|
+
- All webpack paths use forward slashes (Node.js standard)
|
|
1545
|
+
- Use `path.join()` and `path.resolve()` in webpack config, not hardcoded separators
|
|
1546
|
+
- Build scripts use `yarn` (cross-platform) rather than shell-specific commands
|
|
1547
|
+
|
|
1548
|
+
### Duplicate component names
|
|
1549
|
+
|
|
1550
|
+
If `--components` contains duplicates, deduplicate silently and warn:
|
|
1551
|
+
```
|
|
1552
|
+
Warning: Duplicate component name 'OrderTracker' removed from list.
|
|
1553
|
+
```
|
|
1554
|
+
|
|
1555
|
+
### Component name validation
|
|
1556
|
+
|
|
1557
|
+
Reject component names that:
|
|
1558
|
+
- Start with a lowercase letter or digit
|
|
1559
|
+
- Contain hyphens, underscores, or spaces (must be PascalCase)
|
|
1560
|
+
- Are empty or longer than 50 characters
|
|
1561
|
+
- Conflict with built-in Mystique components (e.g., `Card`, `List`, `Loading`)
|
|
1562
|
+
|
|
1563
|
+
## React Hooks API Reference
|
|
1564
|
+
|
|
1565
|
+
The Mystique host app provides 6 hooks via `mystique/hooks/*`. Type declarations are generated in `@types/mystique/hooks.d.ts`. Here is the runtime API for each:
|
|
1566
|
+
|
|
1567
|
+
### useAuth — User Session & Context
|
|
1568
|
+
|
|
1569
|
+
```typescript
|
|
1570
|
+
import { useAuth } from 'mystique/hooks/useAuth';
|
|
1571
|
+
|
|
1572
|
+
const auth = useAuth();
|
|
1573
|
+
|
|
1574
|
+
// User info
|
|
1575
|
+
auth.user.username; // string
|
|
1576
|
+
auth.user.firstName; // string
|
|
1577
|
+
auth.user.timezone; // string (e.g., "Australia/Sydney")
|
|
1578
|
+
auth.user.language; // string (e.g., "en")
|
|
1579
|
+
auth.user.roles; // { role: { name: string } }[]
|
|
1580
|
+
|
|
1581
|
+
// Current context
|
|
1582
|
+
auth.context.current.contextId; // string — retailer or location ID
|
|
1583
|
+
auth.context.current.contextType; // "RETAILER" | "LOCATION"
|
|
1584
|
+
auth.context.current.details?.ref; // string — entity ref
|
|
1585
|
+
|
|
1586
|
+
// Actions
|
|
1587
|
+
auth.switchContext({ contextType: 'RETAILER', contextId });
|
|
1588
|
+
auth.logout();
|
|
1589
|
+
```
|
|
1590
|
+
|
|
1591
|
+
**Use case**: Role-based rendering, context-aware queries, user info display.
|
|
1592
|
+
|
|
1593
|
+
### useQuery — GraphQL Data Fetching
|
|
1594
|
+
|
|
1595
|
+
```typescript
|
|
1596
|
+
import { useQuery } from 'mystique/hooks/useQuery';
|
|
1597
|
+
|
|
1598
|
+
const [result, { loading, error, refetch }] = useQuery<ResultType>(
|
|
1599
|
+
queryString,
|
|
1600
|
+
{ variables }
|
|
1601
|
+
);
|
|
1602
|
+
|
|
1603
|
+
// result.data — typed query result (null while loading)
|
|
1604
|
+
// loading — boolean
|
|
1605
|
+
// error — Error object or null
|
|
1606
|
+
// refetch — () => void — re-execute query
|
|
1607
|
+
```
|
|
1608
|
+
|
|
1609
|
+
Results follow the Relay connection pattern: `edges[].node`, `pageInfo { hasNextPage, endCursor }`.
|
|
1610
|
+
|
|
1611
|
+
**Prefer page query data over useQuery** — only use useQuery for follow-up queries not available in the page data context. Page queries are defined in the manifest `data` block and auto-executed.
|
|
1612
|
+
|
|
1613
|
+
### useSettings — Dynamic Configuration
|
|
1614
|
+
|
|
1615
|
+
```typescript
|
|
1616
|
+
import { useSettings } from 'mystique/hooks/useSettings';
|
|
1617
|
+
|
|
1618
|
+
const settings = useSettings({ key: 'my.setting.name' });
|
|
1619
|
+
|
|
1620
|
+
// Result shape:
|
|
1621
|
+
// settings.key.status — 'loading' | 'error' | (truthy on success)
|
|
1622
|
+
// settings.key.result?.value — the setting value (any type)
|
|
1623
|
+
```
|
|
1624
|
+
|
|
1625
|
+
**Hierarchy**: Location → Retailer → Account → Global (cascades up). A Location-level setting overrides Retailer, which overrides Account.
|
|
1626
|
+
|
|
1627
|
+
### useI18n — Internationalization
|
|
1628
|
+
|
|
1629
|
+
```typescript
|
|
1630
|
+
import { useI18n } from 'mystique/hooks/useI18n';
|
|
1631
|
+
|
|
1632
|
+
const { translate, translateOr, languages, currentLanguage } = useI18n();
|
|
1633
|
+
|
|
1634
|
+
translate('fc.om.orders.title'); // single key lookup
|
|
1635
|
+
translateOr([ // fallback chain
|
|
1636
|
+
'acme.errors.specificError',
|
|
1637
|
+
'acme.errors.genericError',
|
|
1638
|
+
'An error occurred' // last item = default
|
|
1639
|
+
]);
|
|
1640
|
+
currentLanguage.label; // e.g., "English"
|
|
1641
|
+
languages; // { value, label }[]
|
|
1642
|
+
```
|
|
1643
|
+
|
|
1644
|
+
Language dictionaries auto-load from the setting matching the user's language preference (e.g., `LANGUAGE_JA-JP`).
|
|
1645
|
+
|
|
1646
|
+
### useData — Page Query Access
|
|
1647
|
+
|
|
1648
|
+
```typescript
|
|
1649
|
+
import { useData } from 'mystique/hooks/useData';
|
|
1650
|
+
|
|
1651
|
+
const { data, variables, loading, error } = useData();
|
|
1652
|
+
|
|
1653
|
+
data.order; // page query result, scoped by component's dataSource
|
|
1654
|
+
variables.orderId; // query variables from manifest
|
|
1655
|
+
```
|
|
1656
|
+
|
|
1657
|
+
**Use case**: Components that interact with page-level data (filters, list controls) without needing their own query.
|
|
1658
|
+
|
|
1659
|
+
### useEnv — Environment Details
|
|
1660
|
+
|
|
1661
|
+
```typescript
|
|
1662
|
+
import { useEnv } from 'mystique/hooks/useEnv';
|
|
1663
|
+
|
|
1664
|
+
const env = useEnv();
|
|
1665
|
+
|
|
1666
|
+
env.account; // Account name
|
|
1667
|
+
env.app; // App name: "oms", "store", "servicepoint", or custom
|
|
1668
|
+
env.api; // API endpoint URL
|
|
1669
|
+
```
|
|
1670
|
+
|
|
1671
|
+
## Advanced Component Patterns
|
|
1672
|
+
|
|
1673
|
+
### DynamicValue — User-Configurable Display
|
|
1674
|
+
|
|
1675
|
+
`<DynamicValue />` renders manifest-configured attributes as text, image, or embedded component. This is the key to making components reusable across different manifest contexts:
|
|
1676
|
+
|
|
1677
|
+
```typescript
|
|
1678
|
+
import { DynamicValue } from 'mystique/components/DynamicValue';
|
|
1679
|
+
|
|
1680
|
+
// GOOD — configurer controls what is displayed
|
|
1681
|
+
export const MyComponent: FC<ComponentProps> = ({ attribute, data }) => {
|
|
1682
|
+
return <DynamicValue attribute={attribute} context={data} defaultValue={<span>-</span>} />;
|
|
1683
|
+
};
|
|
1684
|
+
|
|
1685
|
+
// AVOID — hardcoded field access, not reusable
|
|
1686
|
+
export const MyComponent: FC<ComponentProps> = ({ data }) => {
|
|
1687
|
+
return <div>{data.order.ref}</div>;
|
|
1688
|
+
};
|
|
1689
|
+
```
|
|
1690
|
+
|
|
1691
|
+
### Children — Manifest-Driven Composition
|
|
1692
|
+
|
|
1693
|
+
`<Children />` renders whatever descendant components are configured in the manifest. This lets configurers compose UI without touching code:
|
|
1694
|
+
|
|
1695
|
+
```typescript
|
|
1696
|
+
import { Children } from 'mystique/components/Children';
|
|
1697
|
+
|
|
1698
|
+
// Render descendants from manifest
|
|
1699
|
+
export const MyAccordion: FC = () => (
|
|
1700
|
+
<CollapsibleDiv>
|
|
1701
|
+
<Children />
|
|
1702
|
+
</CollapsibleDiv>
|
|
1703
|
+
);
|
|
1704
|
+
|
|
1705
|
+
// Override data context for all descendants
|
|
1706
|
+
export const MyDataWrapper: FC = ({ data }) => {
|
|
1707
|
+
const transformed = transformData(data);
|
|
1708
|
+
return (
|
|
1709
|
+
<div>
|
|
1710
|
+
<Children dataOverride={transformed} />
|
|
1711
|
+
</div>
|
|
1712
|
+
);
|
|
1713
|
+
};
|
|
1714
|
+
```
|
|
1715
|
+
|
|
1716
|
+
### TemplateRegistry.render — Resolve Manifest Templates in Code
|
|
1717
|
+
|
|
1718
|
+
When your component receives a template string from the manifest (e.g., `"{{order.ref}} - {{order.status}}"`), resolve it at runtime:
|
|
1719
|
+
|
|
1720
|
+
```typescript
|
|
1721
|
+
import { TemplateRegistry } from 'mystique/registry/TemplateRegistry';
|
|
1722
|
+
|
|
1723
|
+
const resolved = TemplateRegistry.render('{{order.ref}} - {{order.status}}', data);
|
|
1724
|
+
```
|
|
1725
|
+
|
|
1726
|
+
### FieldRegistry.get — Compose Existing Fields
|
|
1727
|
+
|
|
1728
|
+
Reuse OOTB field components inside custom composite fields:
|
|
1729
|
+
|
|
1730
|
+
```typescript
|
|
1731
|
+
import { FieldRegistry } from 'mystique/registry/FieldRegistry';
|
|
1732
|
+
|
|
1733
|
+
const QuantityField = FieldRegistry.get('number');
|
|
1734
|
+
|
|
1735
|
+
// Use in your component's render:
|
|
1736
|
+
<QuantityField name="qty" label="Quantity" value={qty} onChange={setQty} />
|
|
1737
|
+
```
|
|
1738
|
+
|
|
1739
|
+
### onChange + useEffect — Loop Prevention (CRITICAL)
|
|
1740
|
+
|
|
1741
|
+
Form field `onChange` MUST be wrapped in `useEffect`. Calling `onChange` directly in a handler causes infinite re-renders:
|
|
1742
|
+
|
|
1743
|
+
```typescript
|
|
1744
|
+
// CORRECT — useEffect breaks the render cycle
|
|
1745
|
+
const [value, setValue] = useState('');
|
|
1746
|
+
useEffect(() => {
|
|
1747
|
+
onChange({ value });
|
|
1748
|
+
}, [value]);
|
|
1749
|
+
|
|
1750
|
+
// WRONG — causes infinite loop
|
|
1751
|
+
const handleChange = (v: string) => {
|
|
1752
|
+
setValue(v);
|
|
1753
|
+
onChange({ value: v }); // triggers parent re-render → this component re-renders → calls onChange again
|
|
1754
|
+
};
|
|
1755
|
+
```
|
|
1756
|
+
|
|
1757
|
+
### User Action Attribute ↔ Field Mapping
|
|
1758
|
+
|
|
1759
|
+
Workflow user actions define form attributes with a `type` field. Mystique auto-selects the matching FieldRegistry entry:
|
|
1760
|
+
|
|
1761
|
+
| Workflow Attribute Type | FieldRegistry Key | Component |
|
|
1762
|
+
|------------------------|-------------------|-----------|
|
|
1763
|
+
| `STRING` | `text` | Text input |
|
|
1764
|
+
| `INTEGER` | `number` | Number input |
|
|
1765
|
+
| `BOOLEAN` | `boolean` | Checkbox |
|
|
1766
|
+
| `JSON` | `json` | JSON editor |
|
|
1767
|
+
| `<custom>` | `<custom>` | Your registered field |
|
|
1768
|
+
|
|
1769
|
+
Register a custom field to handle custom types: `FieldRegistry.register(['stockReservation'], StockReservationField)`.
|
|
1770
|
+
|
|
1771
|
+
### Material-UI Version Lock
|
|
1772
|
+
|
|
1773
|
+
The host OMX app ships **Material-UI v4.12.4**. This version is locked — do NOT use MUI v5/v6 APIs. Some newer patterns (certain keyframe syntax, `sx` prop, Emotion-based `styled()`) are unsupported.
|
|
1774
|
+
|
|
1775
|
+
Theme colors: primary `#00A9E0`, secondary (brand purple), plus `success`, `error`, `warning`, `info` from the theme palette. Access via `makeStyles`:
|
|
1776
|
+
|
|
1777
|
+
```typescript
|
|
1778
|
+
const useStyles = makeStyles((theme) => ({
|
|
1779
|
+
root: { color: theme.palette.primary.main }, // #00A9E0
|
|
1780
|
+
error: { color: theme.palette.error.main },
|
|
1781
|
+
}));
|
|
1782
|
+
```
|
|
1783
|
+
|
|
1784
|
+
Responsive breakpoints: `theme.breakpoints.down('sm')`, `theme.breakpoints.up('md')`.
|
|
1785
|
+
|
|
1786
|
+
## Session Tracking
|
|
1787
|
+
|
|
1788
|
+
When invoked, log the following to the session tracking protocol (consumed by `/fluent-session-summary` and `/fluent-session-audit-export`):
|
|
1789
|
+
|
|
1790
|
+
**On entry:**
|
|
1791
|
+
```json
|
|
1792
|
+
{ "skill": "fluent-mystique-scaffold", "timestamp": "<ISO-8601>", "arguments": { "projectName": "<name>", "components": ["<list>"], "category": "<cat>" } }
|
|
1793
|
+
```
|
|
1794
|
+
|
|
1795
|
+
**On exit:**
|
|
1796
|
+
```json
|
|
1797
|
+
{ "skill": "fluent-mystique-scaffold", "outcome": "<completed|failed|skipped>", "changesProduced": ["<seq-numbers>"], "toolCallsProduced": ["<seq-numbers>"], "nextRecommended": "/fluent-mystique-builder" }
|
|
1798
|
+
```
|
|
1799
|
+
|
|
1800
|
+
## Handoff
|
|
1801
|
+
|
|
1802
|
+
| Output Artifact | Consumed By | Path |
|
|
1803
|
+
|----------------|-------------|------|
|
|
1804
|
+
| SDK project with components | `/fluent-mystique-builder` (creates manifest referencing components) | `accounts/<PROFILE>/SOURCE/<project-name>/` |
|
|
1805
|
+
| Built bundle | Manifest `plugins[]` array (loaded at runtime by host app) | `accounts/<PROFILE>/SOURCE/<project-name>/dist/bundle.[hash].js` |
|
|
1806
|
+
| Compiled manifests | `/fluent-mystique-validate` (validates before deploy) | `accounts/<PROFILE>/SOURCE/<project-name>/dist/manifests/` |
|
|
1807
|
+
|
|
1808
|
+
## Error Reporting
|
|
1809
|
+
|
|
1810
|
+
Errors from this skill follow the standard format:
|
|
1811
|
+
|
|
1812
|
+
| Field | Description |
|
|
1813
|
+
|-------|-------------|
|
|
1814
|
+
| `phase` | Skill phase where error occurred (e.g., `pre-check`, `validation`, `generation`, `verification`) |
|
|
1815
|
+
| `severity` | `CRITICAL` (blocks downstream), `HIGH` (needs fix), `MEDIUM` (advisory), `LOW` (informational) |
|
|
1816
|
+
| `message` | Human-readable error description |
|
|
1817
|
+
| `resolution` | Suggested fix or downstream skill to invoke |
|
|
1818
|
+
|
|
1819
|
+
## Gotchas
|
|
1820
|
+
|
|
1821
|
+
- **`lib/mystique.js` is ~5MB** and `lib/material-ui.js` is ~1.2MB. These are bundled Fluent Commerce SDK files that cannot be generated -- they must be copied from the reference SDK distribution. Without them, Jest and Storybook will fail with `Cannot read properties of undefined (reading 'ComponentRegistry')`.
|
|
1822
|
+
- **Webpack externals are order-sensitive.** The three externals (`static map`, `externalMaterialUI`, `externalMystique`) must be in an array. If any handler catches a module it should not, the bundle will break at runtime.
|
|
1823
|
+
- **`@material-ui/core` subpath imports** (e.g., `@material-ui/core/Button`) are routed to `window.MaterialUI.Button` by the `externalMaterialUI` function. The `styles` subpath is special-cased to return the root `MaterialUI` object.
|
|
1824
|
+
- **`mystique/*` imports** (e.g., `mystique/hooks/useQuery`) all resolve to `window.Mystique` at runtime. The TypeScript type declarations in `@types/mystique/` provide compile-time safety, but the actual objects come from the host app global.
|
|
1825
|
+
- **React 17 is required.** The host Fluent OMX app provides React 17. Using React 18 APIs will fail at runtime even if they compile successfully.
|
|
1826
|
+
- **Content hash in filename** means the URL changes on every build. The `version.json` file is the stable discovery mechanism -- the manifest `plugins[].src` should point to the directory (with trailing `/`), not the specific bundle file.
|
|
1827
|
+
- **CORS headers are essential** for local development. Without `Access-Control-Allow-Origin: *` on the dev server, the host app cannot load the bundle from localhost:3001.
|
|
1828
|
+
- **Storybook 6.5.x** is the last version compatible with this SDK's webpack 5 setup. Do not upgrade to Storybook 7+ without verifying compatibility.
|
|
1829
|
+
- **The `@emotion/react` external** is mapped to `emotionReact` (not `Emotion`). This matches how `lib/init.js` sets `globalThis.emotionReact = Emotion`.
|
|
1830
|
+
- **Manifest `plugins[]` type field:** Use `{ "type": "url", "src": "..." }` for URL-based plugins (production bundles) or `{ "type": "setting", "setting": "..." }` for setting-based plugin references. Local development always uses the URL type.
|