Skip to content

Commit 8a579df

Browse files
fix: support private child elements in browser esm
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e6ef621 commit 8a579df

20 files changed

Lines changed: 377 additions & 32 deletions

docs/PACKAGING_ARCHITECTURE.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,18 @@ The shared browser ESM config in `tools/vite/element-browser.config.ts` runs fro
9595
the element package directory. It discovers existing `src/delivery`,
9696
`src/author`, `src/print`, and `src/controller` entries, writes them to
9797
`dist/browser`, and only externalizes bare imports listed in
98-
`tools/vite/browser-esm-policy.json`. It also strips package-time custom element
99-
registration because players own tag registration.
98+
`tools/vite/browser-esm-policy.json`. It injects the owning package name/version
99+
as build-time constants so browser artifacts can derive deterministic,
100+
version-scoped private child custom element tags.
101+
102+
Browser ESM keeps the same registration boundary as the runtime contract:
103+
players own authored top-level PIE tag registration, while an element package
104+
owns the package-private child custom elements it renders internally. This is
105+
why private child definitions such as EBSR's multiple-choice parts are kept in
106+
the browser artifact instead of being discovered by `pie-players` at runtime.
107+
Whether or not EBSR's "two multiple-choice children" design is the ideal
108+
architecture, it is the behavior that existing IIFE bundles have shipped for a
109+
long time, so browser ESM must support it for compatibility.
100110

101111
### Browser ESM CommonJS Interop
102112

docs/PIE_ELEMENT_CONTRACT.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,23 @@ When a helper is exported, its name and behavior are public API. Do not rename o
3535

3636
### Custom Element Runtime
3737

38-
Published element view modules export custom element classes. They must not register tags at package import time. Runtime registration belongs to the player or host.
38+
Published element view modules export custom element classes. They must not
39+
register their authored top-level PIE tag at package import time. Runtime
40+
registration of authored item tags belongs to the player or host.
41+
42+
If a package renders private child custom elements inside its own implementation,
43+
the package's browser artifact must define those private child tags itself before
44+
the first render depends on them. Private child tags are not authored content
45+
dependencies and players must not discover them from package dependencies, DOM
46+
snippets, or element-specific knowledge.
47+
48+
Private child tag names are global custom element names, so packages must scope
49+
them by the owning package version (for example with a `--version-<encoded>`
50+
suffix), use the same scoped name for registration and rendering, and keep
51+
registration idempotent with `customElements.get(...)` guards. This preserves
52+
side-by-side loading of multiple package versions and mirrors the IIFE behavior
53+
where private child implementations are resolved from the owning package's
54+
dependency tree at build time.
3955

4056
Players set data via properties, not attributes:
4157

packages/elements-react/complex-rubric/src/author/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@ import debug from 'debug';
1717
import { defaults } from '@pie-element/shared-lodash';
1818
import Main from './main.js';
1919
import sensibleDefaults from './defaults.js';
20+
import {
21+
COMPLEX_RUBRIC_MULTI_TRAIT_CONFIGURE_TAG,
22+
COMPLEX_RUBRIC_SIMPLE_CONFIGURE_TAG,
23+
} from '../private-tags.js';
2024

2125
const MODEL_UPDATED = ModelUpdatedEvent.TYPE;
22-
const RUBRIC_TAG_NAME = 'rubric-configure';
23-
const MULTI_TRAIT_RUBRIC_TAG_NAME = 'multi-trait-rubric-configure';
26+
const RUBRIC_TAG_NAME = COMPLEX_RUBRIC_SIMPLE_CONFIGURE_TAG;
27+
const MULTI_TRAIT_RUBRIC_TAG_NAME = COMPLEX_RUBRIC_MULTI_TRAIT_CONFIGURE_TAG;
2428

2529
class ComplexSimpleRubricConfigure extends RubricConfigure {}
2630

@@ -159,8 +163,10 @@ export default class ComplexRubricConfigureElement extends HTMLElement {
159163
let element = React.createElement(Main, {
160164
model: this._model,
161165
configuration: this._configuration,
166+
multiTraitRubricTagName: MULTI_TRAIT_RUBRIC_TAG_NAME,
162167
onModelChanged: this.onModelChanged,
163168
onConfigurationChanged: this.onConfigurationChanged,
169+
simpleRubricTagName: RUBRIC_TAG_NAME,
164170
canUpdateModel: this.canUpdateModel
165171
});
166172

packages/elements-react/complex-rubric/src/author/main.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import RadioGroup from '@mui/material/RadioGroup';
1717
import FormControlLabel from '@mui/material/FormControlLabel';
1818
import { styled } from '@mui/material/styles';
1919
import { color } from '@pie-lib/render-ui';
20+
import {
21+
COMPLEX_RUBRIC_MULTI_TRAIT_CONFIGURE_TAG,
22+
COMPLEX_RUBRIC_SIMPLE_CONFIGURE_TAG,
23+
} from '../private-tags.js';
2024

2125
const StyledFormControlLabel: any = styled(FormControlLabel)({
2226
'&.MuiFormControlLabel-root': {
@@ -45,9 +49,11 @@ export class Main extends React.Component {
4549
static propTypes = {
4650
canUpdateModel: PropTypes.bool,
4751
configuration: PropTypes.object,
52+
multiTraitRubricTagName: PropTypes.string,
4853
model: PropTypes.object,
4954
onModelChanged: PropTypes.func,
5055
onConfigurationChanged: PropTypes.func,
56+
simpleRubricTagName: PropTypes.string,
5157
};
5258

5359
onModelChanged: any = (model) => {
@@ -64,7 +70,15 @@ export class Main extends React.Component {
6470
};
6571

6672
render() {
67-
const { model, configuration, canUpdateModel } = this.props;
73+
const {
74+
model,
75+
configuration,
76+
canUpdateModel,
77+
multiTraitRubricTagName = COMPLEX_RUBRIC_MULTI_TRAIT_CONFIGURE_TAG,
78+
simpleRubricTagName = COMPLEX_RUBRIC_SIMPLE_CONFIGURE_TAG,
79+
} = this.props;
80+
const MultiTraitRubricConfigureElement = multiTraitRubricTagName;
81+
const SimpleRubricConfigureElement = simpleRubricTagName;
6882

6983
const { extraCSSRules, rubrics = {} } = model || {};
7084
let { rubricType } = model;
@@ -89,7 +103,7 @@ export class Main extends React.Component {
89103
case RUBRIC_TYPES.SIMPLE_RUBRIC:
90104
default:
91105
rubricTag = (
92-
<rubric-configure
106+
<SimpleRubricConfigureElement
93107
id="simpleRubric"
94108
key="simple-rubric"
95109
ref={(ref) => {
@@ -105,7 +119,7 @@ export class Main extends React.Component {
105119

106120
case RUBRIC_TYPES.MULTI_TRAIT_RUBRIC:
107121
rubricTag = (
108-
<multi-trait-rubric-configure
122+
<MultiTraitRubricConfigureElement
109123
id="multiTraitRubric"
110124
key="multi-trait-rubric"
111125
ref={(ref) => {
@@ -122,7 +136,7 @@ export class Main extends React.Component {
122136

123137
case RUBRIC_TYPES.RUBRICLESS:
124138
rubricTag = (
125-
<rubric-configure
139+
<SimpleRubricConfigureElement
126140
id="rubricless"
127141
key="rubricless"
128142
ref={(ref) => {

packages/elements-react/complex-rubric/src/delivery/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
import Rubric from '@pie-element/rubric';
1212
import MultiTraitRubric from '@pie-element/multi-trait-rubric';
1313
import { RUBRIC_TYPES } from '@pie-lib/rubric';
14+
import { COMPLEX_RUBRIC_MULTI_TRAIT_TAG, COMPLEX_RUBRIC_SIMPLE_TAG } from '../private-tags.js';
1415

15-
const RUBRIC_TAG_NAME = 'complex-rubric-simple';
16-
const MULTI_TRAIT_RUBRIC_TAG_NAME = 'complex-rubric-multi-trait';
16+
const RUBRIC_TAG_NAME = COMPLEX_RUBRIC_SIMPLE_TAG;
17+
const MULTI_TRAIT_RUBRIC_TAG_NAME = COMPLEX_RUBRIC_MULTI_TRAIT_TAG;
1718

1819
class ComplexRubricSimple extends Rubric {}
1920

packages/elements-react/complex-rubric/src/print/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ import Rubric from '@pie-element/rubric';
1313
// here it is actually using multi-trait-rubric/src/index.js, instead of using multi-trait-rubric/src/print.js
1414
import MultiTraitRubric from '@pie-element/multi-trait-rubric';
1515
import { RUBRIC_TYPES } from '@pie-lib/rubric';
16+
import { COMPLEX_RUBRIC_MULTI_TRAIT_TAG, COMPLEX_RUBRIC_SIMPLE_TAG } from '../private-tags.js';
1617

17-
const RUBRIC_TAG_NAME = 'complex-rubric-simple';
18-
const MULTI_TRAIT_RUBRIC_TAG_NAME = 'complex-rubric-multi-trait';
18+
const RUBRIC_TAG_NAME = COMPLEX_RUBRIC_SIMPLE_TAG;
19+
const MULTI_TRAIT_RUBRIC_TAG_NAME = COMPLEX_RUBRIC_MULTI_TRAIT_TAG;
1920

2021
class ComplexRubricSimple extends Rubric {}
2122
class ComplexRubricMultiTrait extends MultiTraitRubric {}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
declare const __PIE_PACKAGE_VERSION__: string | undefined;
2+
3+
const encodeVersionForTag = (version: string): string =>
4+
version
5+
.trim()
6+
.toLowerCase()
7+
.replace(/[.+]/g, '-')
8+
.replace(/[^0-9A-Za-z-]/g, '-')
9+
.replace(/-{2,}/g, '-');
10+
11+
const ownerVersion =
12+
typeof __PIE_PACKAGE_VERSION__ === 'string' && __PIE_PACKAGE_VERSION__.trim()
13+
? __PIE_PACKAGE_VERSION__
14+
: 'local';
15+
const ownerVersionSuffix = `--version-${encodeVersionForTag(ownerVersion)}`;
16+
17+
export const COMPLEX_RUBRIC_SIMPLE_TAG = `complex-rubric-simple${ownerVersionSuffix}`;
18+
export const COMPLEX_RUBRIC_MULTI_TRAIT_TAG = `complex-rubric-multi-trait${ownerVersionSuffix}`;
19+
export const COMPLEX_RUBRIC_SIMPLE_CONFIGURE_TAG = `rubric-configure${ownerVersionSuffix}`;
20+
export const COMPLEX_RUBRIC_MULTI_TRAIT_CONFIGURE_TAG = `multi-trait-rubric-configure${ownerVersionSuffix}`;

packages/elements-react/complex-rubric/vite.config.iife.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
import { existsSync } from 'node:fs';
1+
import { existsSync, readFileSync } from 'node:fs';
22
import { resolve } from 'node:path';
33
import react from '@vitejs/plugin-react';
44
import { defineConfig } from 'vite';
55

6+
const packageJson = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8')) as {
7+
name?: string;
8+
version?: string;
9+
};
10+
611
const resolveWorkspaceEntry = (baseDir: string): string | null => {
712
const candidates = ['index.ts', 'index.tsx', 'index.js', 'index.jsx'];
813
for (const candidate of candidates) {
@@ -55,6 +60,8 @@ export default defineConfig({
5560
react(),
5661
],
5762
define: {
63+
__PIE_PACKAGE_NAME__: JSON.stringify(packageJson.name ?? ''),
64+
__PIE_PACKAGE_VERSION__: JSON.stringify(packageJson.version ?? 'local'),
5865
'process.env.NODE_ENV': JSON.stringify('production'),
5966
},
6067
build: {

packages/elements-react/complex-rubric/vite.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
import { readFileSync } from 'node:fs';
12
import { resolve } from 'node:path';
23
import react from '@vitejs/plugin-react';
34
import { defineConfig } from 'vite';
45

6+
const packageJson = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8')) as {
7+
name?: string;
8+
version?: string;
9+
};
10+
511
export default defineConfig(({ mode, command }) => {
612
// Demo mode: serve the docs/demo directory
713
if (mode === 'demo' && command === 'serve') {
@@ -14,6 +20,10 @@ export default defineConfig(({ mode, command }) => {
1420
// Build mode: build the library
1521
return {
1622
plugins: [react()],
23+
define: {
24+
__PIE_PACKAGE_NAME__: JSON.stringify(packageJson.name ?? ''),
25+
__PIE_PACKAGE_VERSION__: JSON.stringify(packageJson.version ?? 'local'),
26+
},
1727
build: {
1828
lib: {
1929
entry: {

packages/elements-react/ebsr/src/author/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ import { ModelUpdatedEvent } from '@pie-element/shared-configure-events';
1414
import MultipleChoiceConfigure from '@pie-element/multiple-choice/author';
1515
import { defaults } from '@pie-element/shared-lodash';
1616
import Main from './main.js';
17+
import { EBSR_MULTIPLE_CHOICE_CONFIGURE_TAG } from '../private-tags.js';
1718

1819
import sensibleDefaults from './defaults.js';
1920

2021
const MODEL_UPDATED = ModelUpdatedEvent.TYPE;
21-
const MC_TAG_NAME = 'ebsr-multiple-choice-configure';
22+
const MC_TAG_NAME = EBSR_MULTIPLE_CHOICE_CONFIGURE_TAG;
2223

2324
class EbsrMCConfigure extends MultipleChoiceConfigure {}
2425
const defineMultipleChoice = () => {
@@ -165,6 +166,7 @@ export default class EbsrConfigure extends HTMLElement {
165166
let element = React.createElement(Main, {
166167
model: this._model,
167168
configuration: this._configuration,
169+
multipleChoiceTagName: MC_TAG_NAME,
168170
onModelChanged: this.onModelChanged,
169171
onConfigurationChanged: this.onConfigurationChanged,
170172
});

0 commit comments

Comments
 (0)