@edynamix/mf-isolation 0.0.31 → 0.0.32

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.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Makes rem lengths in a hosted module's shadow root follow the root font size its own page had, once the module's
3
+ * CSS declares one (see MF_ROOT_FONT_SIZE_PROPERTY): in stylesheets already in the root and every one added or changed
4
+ * later, such as the component styles Angular adds as components first render, and in inline style attributes,
5
+ * including ones inserted or changed later. A module whose CSS never sizes its root is left untouched. Returns a
6
+ * function that stops following; rewritten rules and styles stay rewritten.
7
+ */
8
+ export declare const followModuleRootFontSize: (root: ShadowRoot) => (() => void);
9
+ /**
10
+ * Adapts the CSS a module brings into its shadow root at runtime to the hosted page, for a host to call on each module
11
+ * shadow root it creates and stop on unmount: rem follows the module's own root font size (as followModuleRootFontSize
12
+ * does), and rules a library injects for the document root (`html { --mat-focus-indicator-display: block; }`), which
13
+ * never match inside a shadow root, move onto `:host` (see rescopeDocumentRootSelectors). Safe beside a module's own
14
+ * followModuleRootFontSize on the same root: every step is idempotent, and neither writes anything the other acts on.
15
+ */
16
+ export declare const adaptHostedModuleStyles: (root: ShadowRoot) => (() => void);
17
+ //# sourceMappingURL=hosted-module-styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hosted-module-styles.d.ts","sourceRoot":"","sources":["../src/hosted-module-styles.ts"],"names":[],"mappings":"AAiPA;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,GAAI,MAAM,UAAU,KAAG,CAAC,MAAM,IAAI,CAEtE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,GAAI,MAAM,UAAU,KAAG,CAAC,MAAM,IAAI,CAErE,CAAC","sourcesContent":["import { rescopeDocumentRootSelectors } from './document-root-selectors.js';\nimport {\n MF_ROOT_FONT_SIZE_PROPERTY,\n REM_LENGTH_HINT_PATTERN,\n ROOT_FONT_SIZE_DECLARATION_PATTERN,\n rewriteRemLengths,\n UNFOLLOWED_REM_PATTERN,\n} from './root-font-size.js';\n\nconst INLINE_REM_SELECTOR = '[style*=\"rem\" i]';\n\n// By class name, not instanceof: a root in another window (a frame, or a test's own window) has its own CSSFontFaceRule.\nconst isFontFaceRule = (rule: CSSRule): boolean => {\n return rule.constructor?.name === 'CSSFontFaceRule';\n};\n\n// Rule kinds are told apart by what they carry, not by class: @import has a sheet, style and keyframe rules have\n// declarations, and grouping rules (@media, @supports, @container, @layer, @keyframes, nesting) have child rules.\nconst importedSheetOf = (rule: CSSRule): CSSStyleSheet | undefined => {\n return 'styleSheet' in rule && rule.styleSheet ? (rule.styleSheet as CSSStyleSheet) : undefined;\n};\n\nconst declarationsOf = (rule: CSSRule): CSSStyleDeclaration | undefined => {\n return 'style' in rule && rule.style && !isFontFaceRule(rule) ? (rule.style as CSSStyleDeclaration) : undefined;\n};\n\nconst childRulesOf = (rule: CSSRule): CSSRuleList | undefined => {\n return 'cssRules' in rule && rule.cssRules ? (rule.cssRules as CSSRuleList) : undefined;\n};\n\nconst readRules = (sheet: CSSStyleSheet): CSSRuleList | undefined => {\n try {\n return sheet.cssRules;\n } catch {\n // A cross-origin sheet (a font service's @font-face list) cannot be read, and none of its rules are module rules.\n return undefined;\n }\n};\n\nconst walkRules = (sheet: CSSStyleSheet, visit: (rule: CSSRule) => void): void => {\n const walk = (rules: CSSRuleList): void => {\n for (const rule of Array.from(rules)) {\n const importedSheet = importedSheetOf(rule);\n const importedRules = importedSheet ? readRules(importedSheet) : undefined;\n if (importedRules) {\n walk(importedRules);\n }\n visit(rule);\n const childRules = childRulesOf(rule);\n if (childRules) {\n walk(childRules);\n }\n }\n };\n const rules = readRules(sheet);\n if (rules) {\n walk(rules);\n }\n};\n\n// Only declaration blocks change, through the CSSOM: @media, @container and @supports conditions keep their rem (var()\n// is invalid there, and a media query rem is the browser's initial size anyway), and !important stays with its value.\n// CSSOM changes are not DOM mutations, so no observer, this one or another on the same root, sees them.\nconst followRemInSheet = (sheet: CSSStyleSheet): void => {\n walkRules(sheet, (rule) => {\n const declarations = declarationsOf(rule);\n if (declarations && REM_LENGTH_HINT_PATTERN.test(declarations.cssText)) {\n const rewritten = rewriteRemLengths(declarations.cssText);\n if (rewritten !== declarations.cssText) {\n declarations.cssText = rewritten;\n }\n }\n });\n};\n\nconst rescopeDocumentRootRules = (sheet: CSSStyleSheet): void => {\n walkRules(sheet, (rule) => {\n if (!('selectorText' in rule) || typeof rule.selectorText !== 'string') {\n return;\n }\n const { selectorText } = rescopeDocumentRootSelectors(rule.selectorText);\n if (selectorText !== rule.selectorText) {\n (rule as CSSStyleRule).selectorText = selectorText;\n }\n });\n};\n\nconst declaresRootFontSize = (sheet: CSSStyleSheet): boolean => {\n let declares = false;\n walkRules(sheet, (rule) => {\n declares ||= !!declarationsOf(rule)?.getPropertyValue(MF_ROOT_FONT_SIZE_PROPERTY);\n });\n return declares;\n};\n\n// Inline styles carry rem too, from templates (`style=\"padding-bottom: 0.25rem !important\"`) and from style bindings\n// and code. The whole declaration block is rewritten through element.style, as for sheet rules: !important stays with\n// its value, and a shorthand that also holds var() (whose longhands read empty) is still seen. Material sets inline\n// styles constantly (ripples, overlay positions), so anything without an unfollowed rem length returns at once, and a\n// followed value never looks like work again, which is also why a second observer on the same root writes nothing.\nconst followInlineStyle = (element: Element): void => {\n const attribute = element.getAttribute('style');\n if (!attribute || !UNFOLLOWED_REM_PATTERN.test(attribute)) {\n return;\n }\n const style = (element as HTMLElement).style;\n const declarations = style?.cssText;\n if (!declarations) {\n return;\n }\n const rewritten = rewriteRemLengths(declarations);\n if (rewritten !== declarations) {\n style.cssText = rewritten;\n }\n};\n\nconst followInlineStylesWithin = (node: Node): void => {\n if (node.nodeType !== 1) {\n return;\n }\n const element = node as Element;\n followInlineStyle(element);\n if (element.firstElementChild) {\n for (const descendant of Array.from(element.querySelectorAll(INLINE_REM_SELECTOR))) {\n followInlineStyle(descendant);\n }\n }\n};\n\ntype TStyleOwner = HTMLLinkElement | HTMLStyleElement;\n\nconst isStyleOwner = (node: Node): node is TStyleOwner => {\n return node.nodeName === 'STYLE' || (node.nodeName === 'LINK' && (node as HTMLLinkElement).rel.split(/\\s+/).includes('stylesheet'));\n};\n\ninterface IAdaptStylesOptions {\n documentRootRules: boolean;\n}\n\nconst adaptShadowRootStyles = (root: ShadowRoot, { documentRootRules }: IAdaptStylesOptions): (() => void) => {\n // The root's own window, which need not be the global one (a frame, or a test's own window).\n const RootMutationObserver = root.ownerDocument.defaultView?.MutationObserver ?? MutationObserver;\n let followingRem = false;\n const remSheets = new WeakSet<CSSStyleSheet>();\n const rescopedSheets = new WeakSet<CSSStyleSheet>();\n const watchedStyles = new WeakSet<HTMLStyleElement>();\n\n const owners = (): TStyleOwner[] => Array.from(root.childNodes).filter(isStyleOwner);\n\n const sheetsOf = (styleOwners: TStyleOwner[]): CSSStyleSheet[] => {\n return [...(root.adoptedStyleSheets ?? []), ...styleOwners.map((owner) => owner.sheet).filter((sheet): sheet is CSSStyleSheet => !!sheet)];\n };\n\n // A <style>'s own text is the cheap check; linked and adopted sheets have only their rules.\n const findsRootFontSize = (styleOwners: TStyleOwner[]): boolean => {\n const styles = styleOwners.filter((owner): owner is HTMLStyleElement => owner.nodeName === 'STYLE');\n if (styles.some((style) => ROOT_FONT_SIZE_DECLARATION_PATTERN.test(style.textContent ?? ''))) {\n return true;\n }\n const linkedSheets = styleOwners.filter((owner) => owner.nodeName === 'LINK').map((owner) => owner.sheet);\n return [...(root.adoptedStyleSheets ?? []), ...linkedSheets].some((sheet) => !!sheet && declaresRootFontSize(sheet));\n };\n\n // Started only once the module's CSS declares a root size, so a module without one pays nothing. An element gets its\n // template styles before it is inserted, so insertions are scanned as well as later style attribute changes.\n let inlineObserver: MutationObserver | undefined;\n const followInlineStyles = (): void => {\n for (const element of Array.from(root.querySelectorAll(INLINE_REM_SELECTOR))) {\n followInlineStyle(element);\n }\n inlineObserver = new RootMutationObserver((records) => {\n for (const record of records) {\n if (record.type === 'attributes') {\n followInlineStyle(record.target as Element);\n } else {\n for (const node of Array.from(record.addedNodes)) {\n followInlineStylesWithin(node);\n }\n }\n }\n // Only this callback ran since the records were delivered, so what is queued now are its own rewrites.\n inlineObserver?.takeRecords();\n });\n inlineObserver.observe(root, { attributeFilter: ['style'], attributes: true, childList: true, subtree: true });\n };\n\n const observer = new RootMutationObserver(() => update());\n\n const update = (): void => {\n const styleOwners = owners();\n for (const owner of styleOwners) {\n if (owner.nodeName === 'STYLE' && !watchedStyles.has(owner as HTMLStyleElement)) {\n // Replacing a <style>'s text gives it a new sheet, which must be adapted again.\n observer.observe(owner, { characterData: true, childList: true, subtree: true });\n watchedStyles.add(owner as HTMLStyleElement);\n }\n }\n\n const sheets = sheetsOf(styleOwners);\n if (documentRootRules) {\n for (const sheet of sheets) {\n if (!rescopedSheets.has(sheet)) {\n rescopeDocumentRootRules(sheet);\n rescopedSheets.add(sheet);\n }\n }\n }\n\n followingRem ||= findsRootFontSize(styleOwners);\n if (!followingRem) {\n return;\n }\n if (!inlineObserver) {\n followInlineStyles();\n }\n for (const sheet of sheets) {\n if (!remSheets.has(sheet)) {\n followRemInSheet(sheet);\n remSheets.add(sheet);\n }\n }\n };\n\n // A <link> sheet has no rules until it loads; load does not bubble, so it is caught on the way down.\n const onLoad = (event: Event): void => {\n if (event.target instanceof Node && isStyleOwner(event.target)) {\n update();\n }\n };\n\n observer.observe(root, { childList: true });\n root.addEventListener('load', onLoad, true);\n update();\n\n return () => {\n observer.disconnect();\n inlineObserver?.disconnect();\n root.removeEventListener('load', onLoad, true);\n };\n};\n\n/**\n * Makes rem lengths in a hosted module's shadow root follow the root font size its own page had, once the module's\n * CSS declares one (see MF_ROOT_FONT_SIZE_PROPERTY): in stylesheets already in the root and every one added or changed\n * later, such as the component styles Angular adds as components first render, and in inline style attributes,\n * including ones inserted or changed later. A module whose CSS never sizes its root is left untouched. Returns a\n * function that stops following; rewritten rules and styles stay rewritten.\n */\nexport const followModuleRootFontSize = (root: ShadowRoot): (() => void) => {\n return adaptShadowRootStyles(root, { documentRootRules: false });\n};\n\n/**\n * Adapts the CSS a module brings into its shadow root at runtime to the hosted page, for a host to call on each module\n * shadow root it creates and stop on unmount: rem follows the module's own root font size (as followModuleRootFontSize\n * does), and rules a library injects for the document root (`html { --mat-focus-indicator-display: block; }`), which\n * never match inside a shadow root, move onto `:host` (see rescopeDocumentRootSelectors). Safe beside a module's own\n * followModuleRootFontSize on the same root: every step is idempotent, and neither writes anything the other acts on.\n */\nexport const adaptHostedModuleStyles = (root: ShadowRoot): (() => void) => {\n return adaptShadowRootStyles(root, { documentRootRules: true });\n};\n"]}
@@ -0,0 +1,226 @@
1
+ import { rescopeDocumentRootSelectors } from './document-root-selectors.js';
2
+ import { MF_ROOT_FONT_SIZE_PROPERTY, REM_LENGTH_HINT_PATTERN, ROOT_FONT_SIZE_DECLARATION_PATTERN, rewriteRemLengths, UNFOLLOWED_REM_PATTERN, } from './root-font-size.js';
3
+ const INLINE_REM_SELECTOR = '[style*="rem" i]';
4
+ // By class name, not instanceof: a root in another window (a frame, or a test's own window) has its own CSSFontFaceRule.
5
+ const isFontFaceRule = (rule) => {
6
+ return rule.constructor?.name === 'CSSFontFaceRule';
7
+ };
8
+ // Rule kinds are told apart by what they carry, not by class: @import has a sheet, style and keyframe rules have
9
+ // declarations, and grouping rules (@media, @supports, @container, @layer, @keyframes, nesting) have child rules.
10
+ const importedSheetOf = (rule) => {
11
+ return 'styleSheet' in rule && rule.styleSheet ? rule.styleSheet : undefined;
12
+ };
13
+ const declarationsOf = (rule) => {
14
+ return 'style' in rule && rule.style && !isFontFaceRule(rule) ? rule.style : undefined;
15
+ };
16
+ const childRulesOf = (rule) => {
17
+ return 'cssRules' in rule && rule.cssRules ? rule.cssRules : undefined;
18
+ };
19
+ const readRules = (sheet) => {
20
+ try {
21
+ return sheet.cssRules;
22
+ }
23
+ catch {
24
+ // A cross-origin sheet (a font service's @font-face list) cannot be read, and none of its rules are module rules.
25
+ return undefined;
26
+ }
27
+ };
28
+ const walkRules = (sheet, visit) => {
29
+ const walk = (rules) => {
30
+ for (const rule of Array.from(rules)) {
31
+ const importedSheet = importedSheetOf(rule);
32
+ const importedRules = importedSheet ? readRules(importedSheet) : undefined;
33
+ if (importedRules) {
34
+ walk(importedRules);
35
+ }
36
+ visit(rule);
37
+ const childRules = childRulesOf(rule);
38
+ if (childRules) {
39
+ walk(childRules);
40
+ }
41
+ }
42
+ };
43
+ const rules = readRules(sheet);
44
+ if (rules) {
45
+ walk(rules);
46
+ }
47
+ };
48
+ // Only declaration blocks change, through the CSSOM: @media, @container and @supports conditions keep their rem (var()
49
+ // is invalid there, and a media query rem is the browser's initial size anyway), and !important stays with its value.
50
+ // CSSOM changes are not DOM mutations, so no observer, this one or another on the same root, sees them.
51
+ const followRemInSheet = (sheet) => {
52
+ walkRules(sheet, (rule) => {
53
+ const declarations = declarationsOf(rule);
54
+ if (declarations && REM_LENGTH_HINT_PATTERN.test(declarations.cssText)) {
55
+ const rewritten = rewriteRemLengths(declarations.cssText);
56
+ if (rewritten !== declarations.cssText) {
57
+ declarations.cssText = rewritten;
58
+ }
59
+ }
60
+ });
61
+ };
62
+ const rescopeDocumentRootRules = (sheet) => {
63
+ walkRules(sheet, (rule) => {
64
+ if (!('selectorText' in rule) || typeof rule.selectorText !== 'string') {
65
+ return;
66
+ }
67
+ const { selectorText } = rescopeDocumentRootSelectors(rule.selectorText);
68
+ if (selectorText !== rule.selectorText) {
69
+ rule.selectorText = selectorText;
70
+ }
71
+ });
72
+ };
73
+ const declaresRootFontSize = (sheet) => {
74
+ let declares = false;
75
+ walkRules(sheet, (rule) => {
76
+ declares ||= !!declarationsOf(rule)?.getPropertyValue(MF_ROOT_FONT_SIZE_PROPERTY);
77
+ });
78
+ return declares;
79
+ };
80
+ // Inline styles carry rem too, from templates (`style="padding-bottom: 0.25rem !important"`) and from style bindings
81
+ // and code. The whole declaration block is rewritten through element.style, as for sheet rules: !important stays with
82
+ // its value, and a shorthand that also holds var() (whose longhands read empty) is still seen. Material sets inline
83
+ // styles constantly (ripples, overlay positions), so anything without an unfollowed rem length returns at once, and a
84
+ // followed value never looks like work again, which is also why a second observer on the same root writes nothing.
85
+ const followInlineStyle = (element) => {
86
+ const attribute = element.getAttribute('style');
87
+ if (!attribute || !UNFOLLOWED_REM_PATTERN.test(attribute)) {
88
+ return;
89
+ }
90
+ const style = element.style;
91
+ const declarations = style?.cssText;
92
+ if (!declarations) {
93
+ return;
94
+ }
95
+ const rewritten = rewriteRemLengths(declarations);
96
+ if (rewritten !== declarations) {
97
+ style.cssText = rewritten;
98
+ }
99
+ };
100
+ const followInlineStylesWithin = (node) => {
101
+ if (node.nodeType !== 1) {
102
+ return;
103
+ }
104
+ const element = node;
105
+ followInlineStyle(element);
106
+ if (element.firstElementChild) {
107
+ for (const descendant of Array.from(element.querySelectorAll(INLINE_REM_SELECTOR))) {
108
+ followInlineStyle(descendant);
109
+ }
110
+ }
111
+ };
112
+ const isStyleOwner = (node) => {
113
+ return node.nodeName === 'STYLE' || (node.nodeName === 'LINK' && node.rel.split(/\s+/).includes('stylesheet'));
114
+ };
115
+ const adaptShadowRootStyles = (root, { documentRootRules }) => {
116
+ // The root's own window, which need not be the global one (a frame, or a test's own window).
117
+ const RootMutationObserver = root.ownerDocument.defaultView?.MutationObserver ?? MutationObserver;
118
+ let followingRem = false;
119
+ const remSheets = new WeakSet();
120
+ const rescopedSheets = new WeakSet();
121
+ const watchedStyles = new WeakSet();
122
+ const owners = () => Array.from(root.childNodes).filter(isStyleOwner);
123
+ const sheetsOf = (styleOwners) => {
124
+ return [...(root.adoptedStyleSheets ?? []), ...styleOwners.map((owner) => owner.sheet).filter((sheet) => !!sheet)];
125
+ };
126
+ // A <style>'s own text is the cheap check; linked and adopted sheets have only their rules.
127
+ const findsRootFontSize = (styleOwners) => {
128
+ const styles = styleOwners.filter((owner) => owner.nodeName === 'STYLE');
129
+ if (styles.some((style) => ROOT_FONT_SIZE_DECLARATION_PATTERN.test(style.textContent ?? ''))) {
130
+ return true;
131
+ }
132
+ const linkedSheets = styleOwners.filter((owner) => owner.nodeName === 'LINK').map((owner) => owner.sheet);
133
+ return [...(root.adoptedStyleSheets ?? []), ...linkedSheets].some((sheet) => !!sheet && declaresRootFontSize(sheet));
134
+ };
135
+ // Started only once the module's CSS declares a root size, so a module without one pays nothing. An element gets its
136
+ // template styles before it is inserted, so insertions are scanned as well as later style attribute changes.
137
+ let inlineObserver;
138
+ const followInlineStyles = () => {
139
+ for (const element of Array.from(root.querySelectorAll(INLINE_REM_SELECTOR))) {
140
+ followInlineStyle(element);
141
+ }
142
+ inlineObserver = new RootMutationObserver((records) => {
143
+ for (const record of records) {
144
+ if (record.type === 'attributes') {
145
+ followInlineStyle(record.target);
146
+ }
147
+ else {
148
+ for (const node of Array.from(record.addedNodes)) {
149
+ followInlineStylesWithin(node);
150
+ }
151
+ }
152
+ }
153
+ // Only this callback ran since the records were delivered, so what is queued now are its own rewrites.
154
+ inlineObserver?.takeRecords();
155
+ });
156
+ inlineObserver.observe(root, { attributeFilter: ['style'], attributes: true, childList: true, subtree: true });
157
+ };
158
+ const observer = new RootMutationObserver(() => update());
159
+ const update = () => {
160
+ const styleOwners = owners();
161
+ for (const owner of styleOwners) {
162
+ if (owner.nodeName === 'STYLE' && !watchedStyles.has(owner)) {
163
+ // Replacing a <style>'s text gives it a new sheet, which must be adapted again.
164
+ observer.observe(owner, { characterData: true, childList: true, subtree: true });
165
+ watchedStyles.add(owner);
166
+ }
167
+ }
168
+ const sheets = sheetsOf(styleOwners);
169
+ if (documentRootRules) {
170
+ for (const sheet of sheets) {
171
+ if (!rescopedSheets.has(sheet)) {
172
+ rescopeDocumentRootRules(sheet);
173
+ rescopedSheets.add(sheet);
174
+ }
175
+ }
176
+ }
177
+ followingRem ||= findsRootFontSize(styleOwners);
178
+ if (!followingRem) {
179
+ return;
180
+ }
181
+ if (!inlineObserver) {
182
+ followInlineStyles();
183
+ }
184
+ for (const sheet of sheets) {
185
+ if (!remSheets.has(sheet)) {
186
+ followRemInSheet(sheet);
187
+ remSheets.add(sheet);
188
+ }
189
+ }
190
+ };
191
+ // A <link> sheet has no rules until it loads; load does not bubble, so it is caught on the way down.
192
+ const onLoad = (event) => {
193
+ if (event.target instanceof Node && isStyleOwner(event.target)) {
194
+ update();
195
+ }
196
+ };
197
+ observer.observe(root, { childList: true });
198
+ root.addEventListener('load', onLoad, true);
199
+ update();
200
+ return () => {
201
+ observer.disconnect();
202
+ inlineObserver?.disconnect();
203
+ root.removeEventListener('load', onLoad, true);
204
+ };
205
+ };
206
+ /**
207
+ * Makes rem lengths in a hosted module's shadow root follow the root font size its own page had, once the module's
208
+ * CSS declares one (see MF_ROOT_FONT_SIZE_PROPERTY): in stylesheets already in the root and every one added or changed
209
+ * later, such as the component styles Angular adds as components first render, and in inline style attributes,
210
+ * including ones inserted or changed later. A module whose CSS never sizes its root is left untouched. Returns a
211
+ * function that stops following; rewritten rules and styles stay rewritten.
212
+ */
213
+ export const followModuleRootFontSize = (root) => {
214
+ return adaptShadowRootStyles(root, { documentRootRules: false });
215
+ };
216
+ /**
217
+ * Adapts the CSS a module brings into its shadow root at runtime to the hosted page, for a host to call on each module
218
+ * shadow root it creates and stop on unmount: rem follows the module's own root font size (as followModuleRootFontSize
219
+ * does), and rules a library injects for the document root (`html { --mat-focus-indicator-display: block; }`), which
220
+ * never match inside a shadow root, move onto `:host` (see rescopeDocumentRootSelectors). Safe beside a module's own
221
+ * followModuleRootFontSize on the same root: every step is idempotent, and neither writes anything the other acts on.
222
+ */
223
+ export const adaptHostedModuleStyles = (root) => {
224
+ return adaptShadowRootStyles(root, { documentRootRules: true });
225
+ };
226
+ //# sourceMappingURL=hosted-module-styles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hosted-module-styles.js","sourceRoot":"","sources":["../src/hosted-module-styles.ts"],"names":[],"mappings":"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","sourcesContent":["import { rescopeDocumentRootSelectors } from './document-root-selectors.js';\nimport {\n MF_ROOT_FONT_SIZE_PROPERTY,\n REM_LENGTH_HINT_PATTERN,\n ROOT_FONT_SIZE_DECLARATION_PATTERN,\n rewriteRemLengths,\n UNFOLLOWED_REM_PATTERN,\n} from './root-font-size.js';\n\nconst INLINE_REM_SELECTOR = '[style*=\"rem\" i]';\n\n// By class name, not instanceof: a root in another window (a frame, or a test's own window) has its own CSSFontFaceRule.\nconst isFontFaceRule = (rule: CSSRule): boolean => {\n return rule.constructor?.name === 'CSSFontFaceRule';\n};\n\n// Rule kinds are told apart by what they carry, not by class: @import has a sheet, style and keyframe rules have\n// declarations, and grouping rules (@media, @supports, @container, @layer, @keyframes, nesting) have child rules.\nconst importedSheetOf = (rule: CSSRule): CSSStyleSheet | undefined => {\n return 'styleSheet' in rule && rule.styleSheet ? (rule.styleSheet as CSSStyleSheet) : undefined;\n};\n\nconst declarationsOf = (rule: CSSRule): CSSStyleDeclaration | undefined => {\n return 'style' in rule && rule.style && !isFontFaceRule(rule) ? (rule.style as CSSStyleDeclaration) : undefined;\n};\n\nconst childRulesOf = (rule: CSSRule): CSSRuleList | undefined => {\n return 'cssRules' in rule && rule.cssRules ? (rule.cssRules as CSSRuleList) : undefined;\n};\n\nconst readRules = (sheet: CSSStyleSheet): CSSRuleList | undefined => {\n try {\n return sheet.cssRules;\n } catch {\n // A cross-origin sheet (a font service's @font-face list) cannot be read, and none of its rules are module rules.\n return undefined;\n }\n};\n\nconst walkRules = (sheet: CSSStyleSheet, visit: (rule: CSSRule) => void): void => {\n const walk = (rules: CSSRuleList): void => {\n for (const rule of Array.from(rules)) {\n const importedSheet = importedSheetOf(rule);\n const importedRules = importedSheet ? readRules(importedSheet) : undefined;\n if (importedRules) {\n walk(importedRules);\n }\n visit(rule);\n const childRules = childRulesOf(rule);\n if (childRules) {\n walk(childRules);\n }\n }\n };\n const rules = readRules(sheet);\n if (rules) {\n walk(rules);\n }\n};\n\n// Only declaration blocks change, through the CSSOM: @media, @container and @supports conditions keep their rem (var()\n// is invalid there, and a media query rem is the browser's initial size anyway), and !important stays with its value.\n// CSSOM changes are not DOM mutations, so no observer, this one or another on the same root, sees them.\nconst followRemInSheet = (sheet: CSSStyleSheet): void => {\n walkRules(sheet, (rule) => {\n const declarations = declarationsOf(rule);\n if (declarations && REM_LENGTH_HINT_PATTERN.test(declarations.cssText)) {\n const rewritten = rewriteRemLengths(declarations.cssText);\n if (rewritten !== declarations.cssText) {\n declarations.cssText = rewritten;\n }\n }\n });\n};\n\nconst rescopeDocumentRootRules = (sheet: CSSStyleSheet): void => {\n walkRules(sheet, (rule) => {\n if (!('selectorText' in rule) || typeof rule.selectorText !== 'string') {\n return;\n }\n const { selectorText } = rescopeDocumentRootSelectors(rule.selectorText);\n if (selectorText !== rule.selectorText) {\n (rule as CSSStyleRule).selectorText = selectorText;\n }\n });\n};\n\nconst declaresRootFontSize = (sheet: CSSStyleSheet): boolean => {\n let declares = false;\n walkRules(sheet, (rule) => {\n declares ||= !!declarationsOf(rule)?.getPropertyValue(MF_ROOT_FONT_SIZE_PROPERTY);\n });\n return declares;\n};\n\n// Inline styles carry rem too, from templates (`style=\"padding-bottom: 0.25rem !important\"`) and from style bindings\n// and code. The whole declaration block is rewritten through element.style, as for sheet rules: !important stays with\n// its value, and a shorthand that also holds var() (whose longhands read empty) is still seen. Material sets inline\n// styles constantly (ripples, overlay positions), so anything without an unfollowed rem length returns at once, and a\n// followed value never looks like work again, which is also why a second observer on the same root writes nothing.\nconst followInlineStyle = (element: Element): void => {\n const attribute = element.getAttribute('style');\n if (!attribute || !UNFOLLOWED_REM_PATTERN.test(attribute)) {\n return;\n }\n const style = (element as HTMLElement).style;\n const declarations = style?.cssText;\n if (!declarations) {\n return;\n }\n const rewritten = rewriteRemLengths(declarations);\n if (rewritten !== declarations) {\n style.cssText = rewritten;\n }\n};\n\nconst followInlineStylesWithin = (node: Node): void => {\n if (node.nodeType !== 1) {\n return;\n }\n const element = node as Element;\n followInlineStyle(element);\n if (element.firstElementChild) {\n for (const descendant of Array.from(element.querySelectorAll(INLINE_REM_SELECTOR))) {\n followInlineStyle(descendant);\n }\n }\n};\n\ntype TStyleOwner = HTMLLinkElement | HTMLStyleElement;\n\nconst isStyleOwner = (node: Node): node is TStyleOwner => {\n return node.nodeName === 'STYLE' || (node.nodeName === 'LINK' && (node as HTMLLinkElement).rel.split(/\\s+/).includes('stylesheet'));\n};\n\ninterface IAdaptStylesOptions {\n documentRootRules: boolean;\n}\n\nconst adaptShadowRootStyles = (root: ShadowRoot, { documentRootRules }: IAdaptStylesOptions): (() => void) => {\n // The root's own window, which need not be the global one (a frame, or a test's own window).\n const RootMutationObserver = root.ownerDocument.defaultView?.MutationObserver ?? MutationObserver;\n let followingRem = false;\n const remSheets = new WeakSet<CSSStyleSheet>();\n const rescopedSheets = new WeakSet<CSSStyleSheet>();\n const watchedStyles = new WeakSet<HTMLStyleElement>();\n\n const owners = (): TStyleOwner[] => Array.from(root.childNodes).filter(isStyleOwner);\n\n const sheetsOf = (styleOwners: TStyleOwner[]): CSSStyleSheet[] => {\n return [...(root.adoptedStyleSheets ?? []), ...styleOwners.map((owner) => owner.sheet).filter((sheet): sheet is CSSStyleSheet => !!sheet)];\n };\n\n // A <style>'s own text is the cheap check; linked and adopted sheets have only their rules.\n const findsRootFontSize = (styleOwners: TStyleOwner[]): boolean => {\n const styles = styleOwners.filter((owner): owner is HTMLStyleElement => owner.nodeName === 'STYLE');\n if (styles.some((style) => ROOT_FONT_SIZE_DECLARATION_PATTERN.test(style.textContent ?? ''))) {\n return true;\n }\n const linkedSheets = styleOwners.filter((owner) => owner.nodeName === 'LINK').map((owner) => owner.sheet);\n return [...(root.adoptedStyleSheets ?? []), ...linkedSheets].some((sheet) => !!sheet && declaresRootFontSize(sheet));\n };\n\n // Started only once the module's CSS declares a root size, so a module without one pays nothing. An element gets its\n // template styles before it is inserted, so insertions are scanned as well as later style attribute changes.\n let inlineObserver: MutationObserver | undefined;\n const followInlineStyles = (): void => {\n for (const element of Array.from(root.querySelectorAll(INLINE_REM_SELECTOR))) {\n followInlineStyle(element);\n }\n inlineObserver = new RootMutationObserver((records) => {\n for (const record of records) {\n if (record.type === 'attributes') {\n followInlineStyle(record.target as Element);\n } else {\n for (const node of Array.from(record.addedNodes)) {\n followInlineStylesWithin(node);\n }\n }\n }\n // Only this callback ran since the records were delivered, so what is queued now are its own rewrites.\n inlineObserver?.takeRecords();\n });\n inlineObserver.observe(root, { attributeFilter: ['style'], attributes: true, childList: true, subtree: true });\n };\n\n const observer = new RootMutationObserver(() => update());\n\n const update = (): void => {\n const styleOwners = owners();\n for (const owner of styleOwners) {\n if (owner.nodeName === 'STYLE' && !watchedStyles.has(owner as HTMLStyleElement)) {\n // Replacing a <style>'s text gives it a new sheet, which must be adapted again.\n observer.observe(owner, { characterData: true, childList: true, subtree: true });\n watchedStyles.add(owner as HTMLStyleElement);\n }\n }\n\n const sheets = sheetsOf(styleOwners);\n if (documentRootRules) {\n for (const sheet of sheets) {\n if (!rescopedSheets.has(sheet)) {\n rescopeDocumentRootRules(sheet);\n rescopedSheets.add(sheet);\n }\n }\n }\n\n followingRem ||= findsRootFontSize(styleOwners);\n if (!followingRem) {\n return;\n }\n if (!inlineObserver) {\n followInlineStyles();\n }\n for (const sheet of sheets) {\n if (!remSheets.has(sheet)) {\n followRemInSheet(sheet);\n remSheets.add(sheet);\n }\n }\n };\n\n // A <link> sheet has no rules until it loads; load does not bubble, so it is caught on the way down.\n const onLoad = (event: Event): void => {\n if (event.target instanceof Node && isStyleOwner(event.target)) {\n update();\n }\n };\n\n observer.observe(root, { childList: true });\n root.addEventListener('load', onLoad, true);\n update();\n\n return () => {\n observer.disconnect();\n inlineObserver?.disconnect();\n root.removeEventListener('load', onLoad, true);\n };\n};\n\n/**\n * Makes rem lengths in a hosted module's shadow root follow the root font size its own page had, once the module's\n * CSS declares one (see MF_ROOT_FONT_SIZE_PROPERTY): in stylesheets already in the root and every one added or changed\n * later, such as the component styles Angular adds as components first render, and in inline style attributes,\n * including ones inserted or changed later. A module whose CSS never sizes its root is left untouched. Returns a\n * function that stops following; rewritten rules and styles stay rewritten.\n */\nexport const followModuleRootFontSize = (root: ShadowRoot): (() => void) => {\n return adaptShadowRootStyles(root, { documentRootRules: false });\n};\n\n/**\n * Adapts the CSS a module brings into its shadow root at runtime to the hosted page, for a host to call on each module\n * shadow root it creates and stop on unmount: rem follows the module's own root font size (as followModuleRootFontSize\n * does), and rules a library injects for the document root (`html { --mat-focus-indicator-display: block; }`), which\n * never match inside a shadow root, move onto `:host` (see rescopeDocumentRootSelectors). Safe beside a module's own\n * followModuleRootFontSize on the same root: every step is idempotent, and neither writes anything the other acts on.\n */\nexport const adaptHostedModuleStyles = (root: ShadowRoot): (() => void) => {\n return adaptShadowRootStyles(root, { documentRootRules: true });\n};\n"]}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  export declare const MF_SCOPE_ATTRIBUTE = "data-edyn-mf";
2
2
  export declare const MF_ROOT_SELECTOR = ":mf-root";
3
+ export { scopeDocumentQueries } from './document-queries.js';
4
+ export { type IDocumentRootRescope, rescopeDocumentRootSelectors } from './document-root-selectors.js';
5
+ export { adaptHostedModuleStyles, followModuleRootFontSize } from './hosted-module-styles.js';
3
6
  export * from './navigation.js';
4
- export { followModuleRootFontSize, MF_ROOT_FONT_SIZE_PROPERTY, rewriteRemLengths } from './root-font-size.js';
7
+ export { MF_ROOT_FONT_SIZE_PROPERTY, rewriteRemLengths } from './root-font-size.js';
5
8
  export interface IPrepareMfBoundaryOptions {
6
9
  scopeId: string;
7
10
  theme?: Record<string, string>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAE3C,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,wBAAwB,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAI9G,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,eAAO,MAAM,iBAAiB,GAAI,SAAS,MAAM,KAAG,MAMnD,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,MAEpD,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,SAAS,WAAW,EAAE,oBAAyB,yBAAyB,KAAG,CAAC,MAAM,IAAI,CA2CvH,CAAC","sourcesContent":["export const MF_SCOPE_ATTRIBUTE = 'data-edyn-mf';\nexport const MF_ROOT_SELECTOR = ':mf-root';\n\nexport * from './navigation.js';\nexport { followModuleRootFontSize, MF_ROOT_FONT_SIZE_PROPERTY, rewriteRemLengths } from './root-font-size.js';\n\nconst SCOPE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;\n\nexport interface IPrepareMfBoundaryOptions {\n scopeId: string;\n theme?: Record<string, string>;\n}\n\nexport const validateMfScopeId = (scopeId: string): string => {\n if (!SCOPE_ID_PATTERN.test(scopeId)) {\n throw new Error(`Invalid microfrontend scope id: \"${scopeId}\".`);\n }\n\n return scopeId;\n};\n\nexport const getMfScopeSelector = (scopeId: string): string => {\n return `:where([${MF_SCOPE_ATTRIBUTE}=\"${validateMfScopeId(scopeId)}\"])`;\n};\n\nexport const prepareMfBoundary = (element: HTMLElement, { scopeId, theme = {} }: IPrepareMfBoundaryOptions): (() => void) => {\n validateMfScopeId(scopeId);\n\n for (const name of Object.keys(theme)) {\n if (!name.startsWith('--')) {\n throw new Error(`Microfrontend theme token must be a CSS custom property: \"${name}\".`);\n }\n }\n\n const previousScope = element.getAttribute(MF_SCOPE_ATTRIBUTE);\n const hadRootClass = element.classList.contains('edx-root');\n const previousTheme = new Map<string, { priority: string; value: string }>();\n\n element.setAttribute(MF_SCOPE_ATTRIBUTE, scopeId);\n element.classList.add('edx-root');\n\n for (const [name, value] of Object.entries(theme)) {\n previousTheme.set(name, {\n priority: element.style.getPropertyPriority(name),\n value: element.style.getPropertyValue(name),\n });\n element.style.setProperty(name, value);\n }\n\n return () => {\n if (previousScope === null) {\n element.removeAttribute(MF_SCOPE_ATTRIBUTE);\n } else {\n element.setAttribute(MF_SCOPE_ATTRIBUTE, previousScope);\n }\n\n if (!hadRootClass) {\n element.classList.remove('edx-root');\n }\n\n for (const [name, previous] of previousTheme) {\n if (previous.value) {\n element.style.setProperty(name, previous.value, previous.priority);\n } else {\n element.style.removeProperty(name);\n }\n }\n };\n};\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAE3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,KAAK,oBAAoB,EAAE,4BAA4B,EAAE,MAAM,8BAA8B,CAAC;AACvG,OAAO,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC9F,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAIpF,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,eAAO,MAAM,iBAAiB,GAAI,SAAS,MAAM,KAAG,MAMnD,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,MAEpD,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,SAAS,WAAW,EAAE,oBAAyB,yBAAyB,KAAG,CAAC,MAAM,IAAI,CA2CvH,CAAC","sourcesContent":["export const MF_SCOPE_ATTRIBUTE = 'data-edyn-mf';\nexport const MF_ROOT_SELECTOR = ':mf-root';\n\nexport { scopeDocumentQueries } from './document-queries.js';\nexport { type IDocumentRootRescope, rescopeDocumentRootSelectors } from './document-root-selectors.js';\nexport { adaptHostedModuleStyles, followModuleRootFontSize } from './hosted-module-styles.js';\nexport * from './navigation.js';\nexport { MF_ROOT_FONT_SIZE_PROPERTY, rewriteRemLengths } from './root-font-size.js';\n\nconst SCOPE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;\n\nexport interface IPrepareMfBoundaryOptions {\n scopeId: string;\n theme?: Record<string, string>;\n}\n\nexport const validateMfScopeId = (scopeId: string): string => {\n if (!SCOPE_ID_PATTERN.test(scopeId)) {\n throw new Error(`Invalid microfrontend scope id: \"${scopeId}\".`);\n }\n\n return scopeId;\n};\n\nexport const getMfScopeSelector = (scopeId: string): string => {\n return `:where([${MF_SCOPE_ATTRIBUTE}=\"${validateMfScopeId(scopeId)}\"])`;\n};\n\nexport const prepareMfBoundary = (element: HTMLElement, { scopeId, theme = {} }: IPrepareMfBoundaryOptions): (() => void) => {\n validateMfScopeId(scopeId);\n\n for (const name of Object.keys(theme)) {\n if (!name.startsWith('--')) {\n throw new Error(`Microfrontend theme token must be a CSS custom property: \"${name}\".`);\n }\n }\n\n const previousScope = element.getAttribute(MF_SCOPE_ATTRIBUTE);\n const hadRootClass = element.classList.contains('edx-root');\n const previousTheme = new Map<string, { priority: string; value: string }>();\n\n element.setAttribute(MF_SCOPE_ATTRIBUTE, scopeId);\n element.classList.add('edx-root');\n\n for (const [name, value] of Object.entries(theme)) {\n previousTheme.set(name, {\n priority: element.style.getPropertyPriority(name),\n value: element.style.getPropertyValue(name),\n });\n element.style.setProperty(name, value);\n }\n\n return () => {\n if (previousScope === null) {\n element.removeAttribute(MF_SCOPE_ATTRIBUTE);\n } else {\n element.setAttribute(MF_SCOPE_ATTRIBUTE, previousScope);\n }\n\n if (!hadRootClass) {\n element.classList.remove('edx-root');\n }\n\n for (const [name, previous] of previousTheme) {\n if (previous.value) {\n element.style.setProperty(name, previous.value, previous.priority);\n } else {\n element.style.removeProperty(name);\n }\n }\n };\n};\n"]}
package/dist/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  export const MF_SCOPE_ATTRIBUTE = 'data-edyn-mf';
2
2
  export const MF_ROOT_SELECTOR = ':mf-root';
3
+ export { scopeDocumentQueries } from './document-queries.js';
4
+ export { rescopeDocumentRootSelectors } from './document-root-selectors.js';
5
+ export { adaptHostedModuleStyles, followModuleRootFontSize } from './hosted-module-styles.js';
3
6
  export * from './navigation.js';
4
- export { followModuleRootFontSize, MF_ROOT_FONT_SIZE_PROPERTY, rewriteRemLengths } from './root-font-size.js';
7
+ export { MF_ROOT_FONT_SIZE_PROPERTY, rewriteRemLengths } from './root-font-size.js';
5
8
  const SCOPE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
6
9
  export const validateMfScopeId = (scopeId) => {
7
10
  if (!SCOPE_ID_PATTERN.test(scopeId)) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["export const MF_SCOPE_ATTRIBUTE = 'data-edyn-mf';\nexport const MF_ROOT_SELECTOR = ':mf-root';\n\nexport * from './navigation.js';\nexport { followModuleRootFontSize, MF_ROOT_FONT_SIZE_PROPERTY, rewriteRemLengths } from './root-font-size.js';\n\nconst SCOPE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;\n\nexport interface IPrepareMfBoundaryOptions {\n scopeId: string;\n theme?: Record<string, string>;\n}\n\nexport const validateMfScopeId = (scopeId: string): string => {\n if (!SCOPE_ID_PATTERN.test(scopeId)) {\n throw new Error(`Invalid microfrontend scope id: \"${scopeId}\".`);\n }\n\n return scopeId;\n};\n\nexport const getMfScopeSelector = (scopeId: string): string => {\n return `:where([${MF_SCOPE_ATTRIBUTE}=\"${validateMfScopeId(scopeId)}\"])`;\n};\n\nexport const prepareMfBoundary = (element: HTMLElement, { scopeId, theme = {} }: IPrepareMfBoundaryOptions): (() => void) => {\n validateMfScopeId(scopeId);\n\n for (const name of Object.keys(theme)) {\n if (!name.startsWith('--')) {\n throw new Error(`Microfrontend theme token must be a CSS custom property: \"${name}\".`);\n }\n }\n\n const previousScope = element.getAttribute(MF_SCOPE_ATTRIBUTE);\n const hadRootClass = element.classList.contains('edx-root');\n const previousTheme = new Map<string, { priority: string; value: string }>();\n\n element.setAttribute(MF_SCOPE_ATTRIBUTE, scopeId);\n element.classList.add('edx-root');\n\n for (const [name, value] of Object.entries(theme)) {\n previousTheme.set(name, {\n priority: element.style.getPropertyPriority(name),\n value: element.style.getPropertyValue(name),\n });\n element.style.setProperty(name, value);\n }\n\n return () => {\n if (previousScope === null) {\n element.removeAttribute(MF_SCOPE_ATTRIBUTE);\n } else {\n element.setAttribute(MF_SCOPE_ATTRIBUTE, previousScope);\n }\n\n if (!hadRootClass) {\n element.classList.remove('edx-root');\n }\n\n for (const [name, previous] of previousTheme) {\n if (previous.value) {\n element.style.setProperty(name, previous.value, previous.priority);\n } else {\n element.style.removeProperty(name);\n }\n }\n };\n};\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["export const MF_SCOPE_ATTRIBUTE = 'data-edyn-mf';\nexport const MF_ROOT_SELECTOR = ':mf-root';\n\nexport { scopeDocumentQueries } from './document-queries.js';\nexport { type IDocumentRootRescope, rescopeDocumentRootSelectors } from './document-root-selectors.js';\nexport { adaptHostedModuleStyles, followModuleRootFontSize } from './hosted-module-styles.js';\nexport * from './navigation.js';\nexport { MF_ROOT_FONT_SIZE_PROPERTY, rewriteRemLengths } from './root-font-size.js';\n\nconst SCOPE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;\n\nexport interface IPrepareMfBoundaryOptions {\n scopeId: string;\n theme?: Record<string, string>;\n}\n\nexport const validateMfScopeId = (scopeId: string): string => {\n if (!SCOPE_ID_PATTERN.test(scopeId)) {\n throw new Error(`Invalid microfrontend scope id: \"${scopeId}\".`);\n }\n\n return scopeId;\n};\n\nexport const getMfScopeSelector = (scopeId: string): string => {\n return `:where([${MF_SCOPE_ATTRIBUTE}=\"${validateMfScopeId(scopeId)}\"])`;\n};\n\nexport const prepareMfBoundary = (element: HTMLElement, { scopeId, theme = {} }: IPrepareMfBoundaryOptions): (() => void) => {\n validateMfScopeId(scopeId);\n\n for (const name of Object.keys(theme)) {\n if (!name.startsWith('--')) {\n throw new Error(`Microfrontend theme token must be a CSS custom property: \"${name}\".`);\n }\n }\n\n const previousScope = element.getAttribute(MF_SCOPE_ATTRIBUTE);\n const hadRootClass = element.classList.contains('edx-root');\n const previousTheme = new Map<string, { priority: string; value: string }>();\n\n element.setAttribute(MF_SCOPE_ATTRIBUTE, scopeId);\n element.classList.add('edx-root');\n\n for (const [name, value] of Object.entries(theme)) {\n previousTheme.set(name, {\n priority: element.style.getPropertyPriority(name),\n value: element.style.getPropertyValue(name),\n });\n element.style.setProperty(name, value);\n }\n\n return () => {\n if (previousScope === null) {\n element.removeAttribute(MF_SCOPE_ATTRIBUTE);\n } else {\n element.setAttribute(MF_SCOPE_ATTRIBUTE, previousScope);\n }\n\n if (!hadRootClass) {\n element.classList.remove('edx-root');\n }\n\n for (const [name, previous] of previousTheme) {\n if (previous.value) {\n element.style.setProperty(name, previous.value, previous.priority);\n } else {\n element.style.removeProperty(name);\n }\n }\n };\n};\n"]}
package/dist/postcss.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Plugin } from 'postcss';
1
+ import type { Plugin } from 'postcss';
2
2
  export interface IScopeRemoteCssOptions {
3
3
  scopeId: string;
4
4
  }
@@ -1 +1 @@
1
- {"version":3,"file":"postcss.d.ts","sourceRoot":"","sources":["../src/postcss.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,MAAM,EAAwB,MAAM,SAAS,CAAC;AAMpF,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB;AAqND,eAAO,MAAM,gCAAgC,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,OAEhF,CAAC;AAiYF,eAAO,MAAM,cAAc,GAAI,aAAa,sBAAsB,KAAG,MAWpE,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,aAAa,sBAAsB,KAAG,MAiB3E,CAAC","sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { type Declaration, list, type Plugin, type Root, type Rule } from 'postcss';\nimport selectorParser from 'postcss-selector-parser';\nimport valueParser from 'postcss-value-parser';\nimport { getMfScopeSelector, MF_ROOT_SELECTOR, MF_SCOPE_ATTRIBUTE, validateMfScopeId } from './index.js';\nimport { MF_ROOT_FONT_SIZE_PROPERTY } from './root-font-size.js';\n\nexport interface IScopeRemoteCssOptions {\n scopeId: string;\n}\n\nconst ANGULAR_COMPONENT_STYLE_PATTERN = /\\.component\\.(?:css|less|sass|scss)$/i;\nconst VIEW_ENCAPSULATION_NONE_PATTERN = /(?:^|[,{])\\s*encapsulation\\s*:\\s*(?:[A-Za-z_$][\\w$]*\\s*\\.\\s*)+None\\b/m;\nconst MF_OVERLAY_ATTRIBUTE = 'data-edyn-mf-overlay';\n\ntype TTypeScriptMaskState = 'block-comment' | 'code' | 'double-quote' | 'line-comment' | 'single-quote' | 'template';\n\nconst maskTypeScriptCommentsAndStrings = (source: string): string => {\n let maskedSource = '';\n let state: TTypeScriptMaskState = 'code';\n let escaped = false;\n\n const maskCharacter = (character: string): string => {\n return character === '\\n' || character === '\\r' ? character : ' ';\n };\n\n for (let index = 0; index < source.length; index += 1) {\n const character = source[index] ?? '';\n const nextCharacter = source[index + 1];\n\n if (state === 'code') {\n if (character === '/' && nextCharacter === '/') {\n maskedSource += ' ';\n index += 1;\n state = 'line-comment';\n } else if (character === '/' && nextCharacter === '*') {\n maskedSource += ' ';\n index += 1;\n state = 'block-comment';\n } else if (character === \"'\") {\n maskedSource += ' ';\n state = 'single-quote';\n } else if (character === '\"') {\n maskedSource += ' ';\n state = 'double-quote';\n } else if (character === '`') {\n maskedSource += ' ';\n state = 'template';\n } else {\n maskedSource += character;\n }\n continue;\n }\n\n if (state === 'line-comment') {\n maskedSource += maskCharacter(character);\n if (character === '\\n' || character === '\\r') {\n state = 'code';\n }\n continue;\n }\n\n if (state === 'block-comment') {\n if (character === '*' && nextCharacter === '/') {\n maskedSource += ' ';\n index += 1;\n state = 'code';\n } else {\n maskedSource += maskCharacter(character);\n }\n continue;\n }\n\n maskedSource += maskCharacter(character);\n if (escaped) {\n escaped = false;\n } else if (character === '\\\\') {\n escaped = true;\n } else if (\n (state === 'single-quote' && character === \"'\") ||\n (state === 'double-quote' && character === '\"') ||\n (state === 'template' && character === '`')\n ) {\n state = 'code';\n }\n }\n\n return maskedSource;\n};\n\nconst isKeyframesRule = (rule: Rule): boolean => {\n const parent = rule.parent;\n return parent?.type === 'atrule' && /keyframes$/i.test(parent.name);\n};\n\nconst isRootNode = (node: selectorParser.Node): boolean => {\n return (\n (node.type === 'tag' && (node.value.toLowerCase() === 'html' || node.value.toLowerCase() === 'body')) ||\n (node.type === 'pseudo' && (node.value === ':root' || node.value === MF_ROOT_SELECTOR))\n );\n};\n\nconst isScopeNode = (node: selectorParser.Node | undefined, scopeId: string): boolean => {\n if (node?.type === 'attribute' && node.attribute === MF_SCOPE_ATTRIBUTE && node.operator === '=' && node.value === scopeId) {\n return true;\n }\n\n const normalized = node?.toString().replaceAll('\"', '').replaceAll(\"'\", '');\n return (\n node?.type === 'pseudo' &&\n (normalized === `:where([${MF_SCOPE_ATTRIBUTE}=${scopeId}])` ||\n normalized === `:where([${MF_SCOPE_ATTRIBUTE}=${scopeId}]:not([${MF_OVERLAY_ATTRIBUTE}]))`)\n );\n};\n\nconst startsWithHostPseudo = (selector: selectorParser.Selector): boolean => {\n return selector.first?.type === 'pseudo' && selector.first.value === ':host';\n};\n\nconst removeRootIdentityIds = (selector: selectorParser.Selector): void => {\n let compound: selectorParser.Node[] = [];\n const removeIdsFromRootCompound = (): void => {\n if (compound.some(isRootNode)) {\n for (const node of compound) {\n if (node.type === 'id') {\n node.remove();\n }\n }\n }\n };\n\n for (const node of [...selector.nodes]) {\n if (node.type === 'combinator') {\n removeIdsFromRootCompound();\n compound = [];\n } else {\n compound.push(node);\n }\n }\n removeIdsFromRootCompound();\n};\n\nconst SPECIFICITY_CARRIER = 'edyn-mf-root';\n\n// Legacy rules such as `html body button` or `html body#app .mdc-checkbox__background` are weighed against Angular\n// Material's own rules by the html, body and #app they start from, so the scope that replaces that root must weigh\n// exactly the same. A plain scope attribute (0,1,0) drops the id of `body#app`, letting Material's checked colour win,\n// and outweighs the two types of `html body`, letting an override that lost to `.mat-mdc-button` on the old site win\n// hosted. The root becomes :where(<scope>):not(<carrier>): :where() weighs nothing, and the carrier has the same id,\n// class and type counts as the root part it replaces and never matches the scope element, so :not() always matches and\n// weighs exactly that. Neutral names keep `html`, `body` and `#app` out of the emitted CSS.\nconst rootSpecificityCarrier = (selector: selectorParser.Selector): string => {\n const compounds: selectorParser.Node[][] = [[]];\n for (const node of selector.nodes) {\n if (node.type === 'combinator') {\n compounds.push([]);\n } else {\n compounds.at(-1)?.push(node);\n }\n }\n\n const carrier: string[] = [];\n for (const compound of compounds) {\n if (!compound.some(isRootNode)) {\n continue;\n }\n const types = compound.filter((node) => node.type === 'tag' && isRootNode(node)).length;\n const pseudoRoots = compound.filter((node) => node.type === 'pseudo' && isRootNode(node)).length;\n const ids = compound.filter((node) => node.type === 'id').length;\n carrier.push(\n `${SPECIFICITY_CARRIER.repeat(Math.min(types, 1))}${`.${SPECIFICITY_CARRIER}`.repeat(pseudoRoots)}${`#${SPECIFICITY_CARRIER}`.repeat(ids)}`,\n );\n }\n\n return carrier.filter(Boolean).join(' ');\n};\n\nconst rootedDescendantScope = (selector: selectorParser.Selector, scopeId: string): string => {\n return `:where([${MF_SCOPE_ATTRIBUTE}=\"${scopeId}\"]):not(${rootSpecificityCarrier(selector)})`;\n};\n\nconst isRootOnlySelector = (selector: selectorParser.Selector): boolean => {\n const compounds: selectorParser.Node[][] = [[]];\n\n for (const node of selector.nodes) {\n if (node.type === 'combinator') {\n compounds.push([]);\n } else {\n compounds.at(-1)?.push(node);\n }\n }\n\n return compounds.every((compound) => compound.some(isRootNode));\n};\n\nconst hasRootOnlySelector = (selectorList: string): boolean => {\n let result = false;\n selectorParser((root) => {\n result = root.nodes.some(isRootOnlySelector);\n }).processSync(selectorList);\n return result;\n};\n\nconst getAngularComponentSourcePath = (stylePath: string): string | undefined => {\n const cleanStylePath = stylePath.replace(/[?#].*$/, '');\n if (!ANGULAR_COMPONENT_STYLE_PATTERN.test(cleanStylePath)) {\n return undefined;\n }\n\n return cleanStylePath.replace(/\\.(?:css|less|sass|scss)$/i, '.ts');\n};\n\nconst usesViewEncapsulationNone = (stylePath: string): boolean => {\n const componentSourcePath = getAngularComponentSourcePath(stylePath);\n if (!componentSourcePath || !existsSync(componentSourcePath)) {\n return false;\n }\n\n const componentSource = readFileSync(componentSourcePath, 'utf8');\n return VIEW_ENCAPSULATION_NONE_PATTERN.test(maskTypeScriptCommentsAndStrings(componentSource));\n};\n\nexport const shouldUseAngularComponentScoping = (stylePath: string | undefined): boolean => {\n return !!stylePath && !!getAngularComponentSourcePath(stylePath) && !usesViewEncapsulationNone(stylePath);\n};\n\nconst scopeSelectorList = (selectorList: string, scopeId: string, scopeSelector: string, rootScopeSelectors: string[]): string => {\n return selectorParser((root) => {\n const sourceSelectors = [...root.nodes];\n\n for (const selector of sourceSelectors) {\n const rootNodes = selector.nodes.filter(isRootNode);\n\n if (rootNodes.length > 0) {\n const replacementSelectors = isRootOnlySelector(selector) ? rootScopeSelectors : [rootedDescendantScope(selector, scopeId)];\n\n for (const replacementSelector of replacementSelectors) {\n const variant = selector.clone();\n removeRootIdentityIds(variant);\n const [firstRoot, ...duplicateRoots] = variant.nodes.filter(isRootNode);\n // The replacement can be a compound of several nodes, e.g. :where(<scope>):not(<carrier>).\n const scopeNodes = selectorParser()\n .astSync(replacementSelector)\n .first.nodes.map((node) => node.clone());\n const [scopeNode, ...followingScopeNodes] = scopeNodes;\n if (firstRoot && scopeNode) {\n firstRoot.replaceWith(scopeNode);\n let previous = scopeNode;\n for (const node of followingScopeNodes) {\n variant.insertAfter(previous, node);\n previous = node;\n }\n }\n\n for (const duplicateRoot of duplicateRoots) {\n const previous = duplicateRoot.prev();\n if (previous?.type === 'combinator') {\n previous.remove();\n }\n duplicateRoot.remove();\n }\n\n root.insertBefore(selector, variant);\n }\n selector.remove();\n continue;\n }\n\n if (startsWithHostPseudo(selector) || isScopeNode(selector.first, scopeId)) {\n continue;\n }\n\n const first = selector.first;\n const needsBoundaryVariant = first?.type === 'universal' || (first?.type === 'pseudo' && first.value.startsWith('::'));\n\n if (needsBoundaryVariant) {\n const hasSiblingCombinator = selector.nodes.some((node) => node.type === 'combinator' && (node.value === '+' || node.value === '~'));\n\n if (!hasSiblingCombinator) {\n const boundaryVariant = selector.clone();\n const boundaryScope = selectorParser().astSync(scopeSelector).first.first.clone();\n boundaryVariant.first.rawSpaceBefore = '';\n if (boundaryVariant.first.type === 'universal') {\n boundaryVariant.first.replaceWith(boundaryScope);\n } else {\n boundaryVariant.prepend(boundaryScope);\n }\n root.insertBefore(selector, boundaryVariant);\n }\n }\n\n const descendantScope = selectorParser().astSync(`${scopeSelector} *`).first;\n const prefixNodes = descendantScope.nodes.slice(0, -1).map((node) => node.clone());\n selector.first.rawSpaceBefore = '';\n for (const prefixNode of prefixNodes.reverse()) {\n selector.prepend(prefixNode);\n }\n }\n\n const seenSelectors = new Set<string>();\n for (const selector of [...root.nodes]) {\n const normalizedSelector = selector.toString().trim();\n if (seenSelectors.has(normalizedSelector)) {\n selector.remove();\n } else {\n seenSelectors.add(normalizedSelector);\n }\n }\n }).processSync(selectorList);\n};\n\nconst findFunctionalPseudoEnd = (selector: string, openParenthesisIndex: number): number => {\n let depth = 0;\n let quote: '\"' | \"'\" | undefined;\n\n for (let index = openParenthesisIndex; index < selector.length; index += 1) {\n const character = selector[index];\n const previousCharacter = selector[index - 1];\n\n if (quote) {\n if (character === quote && previousCharacter !== '\\\\') {\n quote = undefined;\n }\n continue;\n }\n\n if (character === '\"' || character === \"'\") {\n quote = character;\n continue;\n }\n\n if (character === '(') {\n depth += 1;\n } else if (character === ')') {\n depth -= 1;\n if (depth === 0) {\n return index;\n }\n }\n }\n\n return -1;\n};\n\nconst scopeAngularSelector = (selector: string, boundarySelector: string): string => {\n const normalizedSelector = selector.trim();\n const angularBoundary = `:host-context(${boundarySelector})`;\n const normalizedBoundary = normalizedSelector.slice(0, angularBoundary.length).replaceAll(\"'\", '\"');\n\n if (normalizedBoundary === angularBoundary) {\n return normalizedSelector;\n }\n\n if (/^:host-context\\(/i.test(normalizedSelector)) {\n return `${angularBoundary}${normalizedSelector}`;\n }\n\n if (/^:host\\(/i.test(normalizedSelector)) {\n const openParenthesisIndex = normalizedSelector.indexOf('(');\n const closeParenthesisIndex = findFunctionalPseudoEnd(normalizedSelector, openParenthesisIndex);\n if (closeParenthesisIndex !== -1) {\n const hostSelector = normalizedSelector.slice(openParenthesisIndex + 1, closeParenthesisIndex);\n const remainder = normalizedSelector.slice(closeParenthesisIndex + 1);\n return list\n .comma(hostSelector)\n .map((hostSelectorVariant) => `${angularBoundary}${hostSelectorVariant}${remainder}`)\n .join(', ');\n }\n }\n\n if (/^:host\\b/i.test(normalizedSelector)) {\n return normalizedSelector.replace(/^:host\\b/i, angularBoundary);\n }\n\n return `${angularBoundary} ${normalizedSelector}`;\n};\n\nconst scopeAngularSelectorList = (selectorList: string, boundarySelector: string): string => {\n return list\n .comma(selectorList)\n .map((selector) => scopeAngularSelector(selector, boundarySelector))\n .join(', ');\n};\n\nconst namespaceKeyframes = (root: Root, keyframePrefix: string): void => {\n const keyframes = new Map<string, string>();\n\n root.walkAtRules((atRule) => {\n if (!/keyframes$/i.test(atRule.name) || atRule.params.startsWith(keyframePrefix)) {\n return;\n }\n\n const originalName = atRule.params.trim();\n const scopedName = `${keyframePrefix}${originalName}`;\n keyframes.set(originalName, scopedName);\n atRule.params = scopedName;\n });\n\n root.walkDecls((declaration) => {\n if (!/^(?:-webkit-)?animation(?:-name)?$/i.test(declaration.prop) && !declaration.prop.startsWith('--animate-')) {\n return;\n }\n\n const parsed = valueParser(declaration.value);\n parsed.walk((node) => {\n if (node.type === 'word') {\n node.value = keyframes.get(node.value) ?? node.value;\n }\n });\n declaration.value = parsed.toString();\n });\n};\n\nconst INITIAL_FONT_SIZE_PX = 16;\n// Absolute-size keywords as browsers compute them, and the relative ones as they resolve on the root, whose parent size\n// is the initial 16px.\nconst FONT_SIZE_KEYWORDS_PX: Readonly<Record<string, number>> = {\n 'xx-small': 9,\n 'x-small': 10,\n small: 13,\n medium: 16,\n large: 18,\n 'x-large': 24,\n 'xx-large': 32,\n 'xxx-large': 48,\n smaller: INITIAL_FONT_SIZE_PX / 1.2,\n larger: INITIAL_FONT_SIZE_PX * 1.2,\n};\n// On the root, every global keyword gives the initial size.\nconst GLOBAL_KEYWORDS = new Set(['inherit', 'initial', 'revert', 'revert-layer', 'unset']);\n// em, rem and % on the root are relative to the initial size.\nconst FONT_SIZE_UNITS_PX: Readonly<Record<string, number>> = {\n '%': INITIAL_FONT_SIZE_PX / 100,\n cm: 96 / 2.54,\n em: INITIAL_FONT_SIZE_PX,\n in: 96,\n mm: 96 / 25.4,\n pc: 16,\n pt: 96 / 72,\n px: 1,\n q: 96 / 101.6,\n rem: INITIAL_FONT_SIZE_PX,\n};\n\nconst fontSizeOfWord = (word: string): number | undefined => {\n const keyword = FONT_SIZE_KEYWORDS_PX[word.toLowerCase()];\n if (keyword !== undefined) {\n return keyword;\n }\n const dimension = valueParser.unit(word);\n const unitPx = dimension ? FONT_SIZE_UNITS_PX[dimension.unit.toLowerCase()] : undefined;\n const amount = Number(dimension ? dimension.number : Number.NaN);\n return unitPx !== undefined && Number.isFinite(amount) && amount >= 0 ? amount * unitPx : undefined;\n};\n\nconst significantValueNodes = (value: string): valueParser.Node[] => {\n return valueParser(value).nodes.filter((node) => node.type !== 'space' && node.type !== 'comment');\n};\n\nconst globalKeywordFontSize = (nodes: valueParser.Node[]): number | undefined => {\n const [only] = nodes;\n return nodes.length === 1 && only?.type === 'word' && GLOBAL_KEYWORDS.has(only.value.toLowerCase()) ? INITIAL_FONT_SIZE_PX : undefined;\n};\n\nconst resolveFontSize = (value: string): number | undefined => {\n const nodes = significantValueNodes(value);\n const [only] = nodes;\n return globalKeywordFontSize(nodes) ?? (nodes.length === 1 && only?.type === 'word' ? fontSizeOfWord(only.value) : undefined);\n};\n\n// `font: [style] [variant] [weight] [stretch] <size>[/<line-height>] <family>`: the size is the word before `/`, or else\n// the first word that is a size. A function (var(), calc()) or a family name reached first means it cannot be read.\nconst resolveFontShorthandSize = (value: string): number | undefined => {\n const nodes = significantValueNodes(value);\n const globalSize = globalKeywordFontSize(nodes);\n if (globalSize !== undefined) {\n return globalSize;\n }\n\n const slashIndex = nodes.findIndex((node) => node.type === 'div' && node.value === '/');\n if (slashIndex > 0) {\n const size = nodes[slashIndex - 1];\n return size?.type === 'word' ? fontSizeOfWord(size.value) : undefined;\n }\n\n for (const node of nodes) {\n if (node.type !== 'word') {\n return undefined;\n }\n const size = fontSizeOfWord(node.value);\n if (size !== undefined) {\n return size;\n }\n }\n return undefined;\n};\n\n// `html` and `:root` size the document root, so rem; `body` and `html body` size only the body.\nconst targetsDocumentRoot = (selector: selectorParser.Selector): boolean => {\n let subject: selectorParser.Node[] = [];\n for (const node of selector.nodes) {\n subject = node.type === 'combinator' ? [] : [...subject, node];\n }\n return subject.some((node) => (node.type === 'tag' && node.value.toLowerCase() === 'html') || (node.type === 'pseudo' && node.value === ':root'));\n};\n\nconst documentRootSelectorsOf = (selectorList: string): string[] => {\n const selectors: string[] = [];\n selectorParser((root) => {\n for (const selector of root.nodes) {\n if (targetsDocumentRoot(selector)) {\n selectors.push(selector.toString().trim());\n }\n }\n }).processSync(selectorList);\n return selectors;\n};\n\ntype TWarn = (node: Declaration, text: string) => void;\n\n// Hosted, rem resolves against the host page's root, not the root size the module's own page set. Each rule that sizes\n// the document root gets a sibling rule right after it that records that size as MF_ROOT_FONT_SIZE_PROPERTY, under the\n// same conditions (@media, importance) and in the same cascade order; scoping then moves it onto the module boundary,\n// where followModuleRootFontSize() makes rem lengths read it. CSS that never sizes the root is left byte-identical.\nconst recordRootFontSize = (root: Root, warn: TWarn): void => {\n root.walkRules((rule) => {\n if (isKeyframesRule(rule)) {\n return;\n }\n\n let sizing: Declaration | undefined;\n for (const node of rule.nodes) {\n if (node.type === 'decl' && /^font(?:-size)?$/i.test(node.prop) && (node.important || !sizing?.important)) {\n sizing = node;\n }\n }\n if (!sizing) {\n return;\n }\n\n const rootSelectors = documentRootSelectorsOf(rule.selector);\n if (rootSelectors.length === 0) {\n return;\n }\n\n const sizePx = sizing.prop.toLowerCase() === 'font' ? resolveFontShorthandSize(sizing.value) : resolveFontSize(sizing.value);\n if (sizePx === undefined) {\n warn(\n sizing,\n `Cannot resolve the document root font size \"${sizing.value}\" at build time; rem lengths in the hosted module will use the host page's root size.`,\n );\n return;\n }\n\n const recorded = rule.clone({ selector: rootSelectors.join(', ') });\n recorded.raws.before = rule.raws.before || '\\n';\n recorded.removeAll();\n // The recorded size points back at the declaration it came from, so source maps keep a source for it.\n recorded.append({\n important: sizing.important,\n prop: MF_ROOT_FONT_SIZE_PROPERTY,\n source: sizing.source,\n value: `${Number(sizePx.toFixed(4))}px`,\n });\n rule.after(recorded);\n });\n};\n\nconst scopeGlobalRoot = (root: Root, scopeId: string, scopeSelector: string, keyframePrefix: string, warn: TWarn): void => {\n recordRootFontSize(root, warn);\n namespaceKeyframes(root, keyframePrefix);\n const contentRootSelector = `:where([${MF_SCOPE_ATTRIBUTE}=\"${scopeId}\"]:not([${MF_OVERLAY_ATTRIBUTE}]))`;\n root.walkRules((rule) => {\n if (!isKeyframesRule(rule)) {\n const declarations = rule.nodes.filter((node) => node.type === 'decl');\n const customProperties = declarations.filter((declaration) => declaration.prop.startsWith('--'));\n const standardDeclarations = declarations.filter((declaration) => !declaration.prop.startsWith('--'));\n\n if (customProperties.length > 0 && standardDeclarations.length > 0 && hasRootOnlySelector(rule.selector)) {\n const customPropertyRule = rule.cloneBefore();\n customPropertyRule.walkDecls((declaration) => {\n if (!declaration.prop.startsWith('--')) {\n declaration.remove();\n }\n });\n customPropertyRule.selector = scopeSelectorList(customPropertyRule.selector, scopeId, scopeSelector, [':host', scopeSelector]);\n rule.walkDecls((declaration) => {\n if (declaration.prop.startsWith('--')) {\n declaration.remove();\n }\n });\n }\n\n const containsOnlyCustomProperties = declarations.length > 0 && declarations.every((declaration) => declaration.prop.startsWith('--'));\n const rootScopeSelectors = containsOnlyCustomProperties ? [':host', scopeSelector] : [':host', contentRootSelector];\n rule.selector = scopeSelectorList(rule.selector, scopeId, scopeSelector, rootScopeSelectors);\n }\n });\n};\n\nconst scopeAngularComponentRoot = (root: Root, boundarySelector: string, keyframePrefix: string): void => {\n namespaceKeyframes(root, keyframePrefix);\n root.walkRules((rule) => {\n if (!isKeyframesRule(rule)) {\n rule.selector = scopeAngularSelectorList(rule.selector, boundarySelector);\n }\n });\n};\n\nexport const scopeRemoteCss = ({ scopeId }: IScopeRemoteCssOptions): Plugin => {\n const validScopeId = validateMfScopeId(scopeId);\n const scopeSelector = getMfScopeSelector(validScopeId);\n const keyframePrefix = `edyn-mf-${validScopeId}--`;\n\n return {\n postcssPlugin: `edyn-mf-isolation-${validScopeId}`,\n Once(root, { result }) {\n scopeGlobalRoot(root, validScopeId, scopeSelector, keyframePrefix, (node, text) => node.warn(result, text));\n },\n };\n};\n\nexport const scopeAngularRemoteCss = ({ scopeId }: IScopeRemoteCssOptions): Plugin => {\n const validScopeId = validateMfScopeId(scopeId);\n const scopeSelector = getMfScopeSelector(validScopeId);\n const boundarySelector = `[${MF_SCOPE_ATTRIBUTE}=\"${validScopeId}\"]`;\n const keyframePrefix = `edyn-mf-${validScopeId}--`;\n\n return {\n postcssPlugin: `edyn-mf-angular-isolation-${validScopeId}`,\n Once(root, { result }) {\n if (shouldUseAngularComponentScoping(root.source?.input.file)) {\n scopeAngularComponentRoot(root, boundarySelector, keyframePrefix);\n return;\n }\n\n scopeGlobalRoot(root, validScopeId, scopeSelector, keyframePrefix, (node, text) => node.warn(result, text));\n },\n };\n};\n"]}
1
+ {"version":3,"file":"postcss.d.ts","sourceRoot":"","sources":["../src/postcss.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAe,MAAM,EAAc,MAAM,SAAS,CAAC;AAM/D,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB;AA2OD,eAAO,MAAM,gCAAgC,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,OAEhF,CAAC;AA0TF,eAAO,MAAM,cAAc,GAAI,aAAa,sBAAsB,KAAG,MAWpE,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,aAAa,sBAAsB,KAAG,MAgB3E,CAAC","sourcesContent":["import { existsSync, readdirSync, readFileSync } from 'node:fs';\nimport { basename, dirname, join } from 'node:path';\nimport type { Declaration, Plugin, Root, Rule } from 'postcss';\nimport selectorParser from 'postcss-selector-parser';\nimport valueParser from 'postcss-value-parser';\nimport { getMfScopeSelector, MF_ROOT_SELECTOR, MF_SCOPE_ATTRIBUTE, validateMfScopeId } from './index.js';\nimport { MF_ROOT_FONT_SIZE_PROPERTY } from './root-font-size.js';\n\nexport interface IScopeRemoteCssOptions {\n scopeId: string;\n}\n\nconst ANGULAR_COMPONENT_STYLE_PATTERN = /\\.component\\.(?:css|less|sass|scss)$/i;\nconst VIEW_ENCAPSULATION_NONE_PATTERN = /(?:^|[,{])\\s*encapsulation\\s*:\\s*(?:[A-Za-z_$][\\w$]*\\s*\\.\\s*)+None\\b/m;\nconst MF_OVERLAY_ATTRIBUTE = 'data-edyn-mf-overlay';\n\ntype TTypeScriptMaskState = 'block-comment' | 'code' | 'double-quote' | 'line-comment' | 'single-quote' | 'template';\n\nconst maskTypeScriptCommentsAndStrings = (source: string): string => {\n let maskedSource = '';\n let state: TTypeScriptMaskState = 'code';\n let escaped = false;\n\n const maskCharacter = (character: string): string => {\n return character === '\\n' || character === '\\r' ? character : ' ';\n };\n\n for (let index = 0; index < source.length; index += 1) {\n const character = source[index] ?? '';\n const nextCharacter = source[index + 1];\n\n if (state === 'code') {\n if (character === '/' && nextCharacter === '/') {\n maskedSource += ' ';\n index += 1;\n state = 'line-comment';\n } else if (character === '/' && nextCharacter === '*') {\n maskedSource += ' ';\n index += 1;\n state = 'block-comment';\n } else if (character === \"'\") {\n maskedSource += ' ';\n state = 'single-quote';\n } else if (character === '\"') {\n maskedSource += ' ';\n state = 'double-quote';\n } else if (character === '`') {\n maskedSource += ' ';\n state = 'template';\n } else {\n maskedSource += character;\n }\n continue;\n }\n\n if (state === 'line-comment') {\n maskedSource += maskCharacter(character);\n if (character === '\\n' || character === '\\r') {\n state = 'code';\n }\n continue;\n }\n\n if (state === 'block-comment') {\n if (character === '*' && nextCharacter === '/') {\n maskedSource += ' ';\n index += 1;\n state = 'code';\n } else {\n maskedSource += maskCharacter(character);\n }\n continue;\n }\n\n maskedSource += maskCharacter(character);\n if (escaped) {\n escaped = false;\n } else if (character === '\\\\') {\n escaped = true;\n } else if (\n (state === 'single-quote' && character === \"'\") ||\n (state === 'double-quote' && character === '\"') ||\n (state === 'template' && character === '`')\n ) {\n state = 'code';\n }\n }\n\n return maskedSource;\n};\n\nconst isKeyframesRule = (rule: Rule): boolean => {\n const parent = rule.parent;\n return parent?.type === 'atrule' && /keyframes$/i.test(parent.name);\n};\n\nconst isRootNode = (node: selectorParser.Node): boolean => {\n return (\n (node.type === 'tag' && (node.value.toLowerCase() === 'html' || node.value.toLowerCase() === 'body')) ||\n (node.type === 'pseudo' && (node.value === ':root' || node.value === MF_ROOT_SELECTOR))\n );\n};\n\nconst isScopeNode = (node: selectorParser.Node | undefined, scopeId: string): boolean => {\n if (node?.type === 'attribute' && node.attribute === MF_SCOPE_ATTRIBUTE && node.operator === '=' && node.value === scopeId) {\n return true;\n }\n\n const normalized = node?.toString().replaceAll('\"', '').replaceAll(\"'\", '');\n return (\n node?.type === 'pseudo' &&\n (normalized === `:where([${MF_SCOPE_ATTRIBUTE}=${scopeId}])` ||\n normalized === `:where([${MF_SCOPE_ATTRIBUTE}=${scopeId}]:not([${MF_OVERLAY_ATTRIBUTE}]))`)\n );\n};\n\nconst startsWithHostPseudo = (selector: selectorParser.Selector): boolean => {\n return selector.first?.type === 'pseudo' && selector.first.value === ':host';\n};\n\nconst removeRootIdentityIds = (selector: selectorParser.Selector): void => {\n let compound: selectorParser.Node[] = [];\n const removeIdsFromRootCompound = (): void => {\n if (compound.some(isRootNode)) {\n for (const node of compound) {\n if (node.type === 'id') {\n node.remove();\n }\n }\n }\n };\n\n for (const node of [...selector.nodes]) {\n if (node.type === 'combinator') {\n removeIdsFromRootCompound();\n compound = [];\n } else {\n compound.push(node);\n }\n }\n removeIdsFromRootCompound();\n};\n\nconst SPECIFICITY_CARRIER = 'edyn-mf-root';\n\n// Legacy rules such as `html body button` or `html body#app .mdc-checkbox__background` are weighed against Angular\n// Material's own rules by the html, body and #app they start from, so the scope that replaces that root must weigh\n// exactly the same. A plain scope attribute (0,1,0) drops the id of `body#app`, letting Material's checked colour win,\n// and outweighs the two types of `html body`, letting an override that lost to `.mat-mdc-button` on the old site win\n// hosted. The root becomes :where(<scope>):not(<carrier>): :where() weighs nothing, and the carrier has the same id,\n// class and type counts as the root part it replaces and never matches the scope element, so :not() always matches and\n// weighs exactly that. Neutral names keep `html`, `body` and `#app` out of the emitted CSS.\nconst rootSpecificityCarrier = (selector: selectorParser.Selector): string => {\n const compounds: selectorParser.Node[][] = [[]];\n for (const node of selector.nodes) {\n if (node.type === 'combinator') {\n compounds.push([]);\n } else {\n compounds.at(-1)?.push(node);\n }\n }\n\n const carrier: string[] = [];\n for (const compound of compounds) {\n if (!compound.some(isRootNode)) {\n continue;\n }\n const types = compound.filter((node) => node.type === 'tag' && isRootNode(node)).length;\n const pseudoRoots = compound.filter((node) => node.type === 'pseudo' && isRootNode(node)).length;\n const ids = compound.filter((node) => node.type === 'id').length;\n carrier.push(\n `${SPECIFICITY_CARRIER.repeat(Math.min(types, 1))}${`.${SPECIFICITY_CARRIER}`.repeat(pseudoRoots)}${`#${SPECIFICITY_CARRIER}`.repeat(ids)}`,\n );\n }\n\n return carrier.filter(Boolean).join(' ');\n};\n\nconst rootedDescendantScope = (selector: selectorParser.Selector, scopeId: string): string => {\n return `:where([${MF_SCOPE_ATTRIBUTE}=\"${scopeId}\"]):not(${rootSpecificityCarrier(selector)})`;\n};\n\nconst isRootOnlySelector = (selector: selectorParser.Selector): boolean => {\n const compounds: selectorParser.Node[][] = [[]];\n\n for (const node of selector.nodes) {\n if (node.type === 'combinator') {\n compounds.push([]);\n } else {\n compounds.at(-1)?.push(node);\n }\n }\n\n return compounds.every((compound) => compound.some(isRootNode));\n};\n\nconst hasRootOnlySelector = (selectorList: string): boolean => {\n let result = false;\n selectorParser((root) => {\n result = root.nodes.some(isRootOnlySelector);\n }).processSync(selectorList);\n return result;\n};\n\nconst escapeRegExp = (text: string): string => text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n// A component stylesheet need not be named *.component.scss (CampaignManagement's create-campaign.ts uses\n// `styleUrl: './create-campaign.scss'`); treated as global CSS, Angular would then encapsulate the boundary itself and\n// the rules could never match. Such a file is a component's when a sibling component source references it.\nconst referencingComponentSourceOf = (stylePath: string): string | undefined => {\n const directory = dirname(stylePath);\n if (!existsSync(directory)) {\n return undefined;\n }\n const reference = new RegExp(`styleUrls?\\\\s*:\\\\s*(?:\\\\[[^\\\\]]*?)?['\"\\`]\\\\./${escapeRegExp(basename(stylePath))}['\"\\`]`);\n for (const fileName of readdirSync(directory)) {\n if (!fileName.endsWith('.ts') || /\\.(?:spec|test|d)\\.ts$/.test(fileName)) {\n continue;\n }\n const sourcePath = join(directory, fileName);\n if (reference.test(readFileSync(sourcePath, 'utf8'))) {\n return sourcePath;\n }\n }\n return undefined;\n};\n\nconst getAngularComponentSourcePath = (stylePath: string): string | undefined => {\n const cleanStylePath = stylePath.replace(/[?#].*$/, '');\n if (ANGULAR_COMPONENT_STYLE_PATTERN.test(cleanStylePath)) {\n return cleanStylePath.replace(/\\.(?:css|less|sass|scss)$/i, '.ts');\n }\n return /\\.(?:css|less|sass|scss)$/i.test(cleanStylePath) ? referencingComponentSourceOf(cleanStylePath) : undefined;\n};\n\nconst usesViewEncapsulationNone = (stylePath: string): boolean => {\n const componentSourcePath = getAngularComponentSourcePath(stylePath);\n if (!componentSourcePath || !existsSync(componentSourcePath)) {\n return false;\n }\n\n const componentSource = readFileSync(componentSourcePath, 'utf8');\n return VIEW_ENCAPSULATION_NONE_PATTERN.test(maskTypeScriptCommentsAndStrings(componentSource));\n};\n\nexport const shouldUseAngularComponentScoping = (stylePath: string | undefined): boolean => {\n return !!stylePath && !!getAngularComponentSourcePath(stylePath) && !usesViewEncapsulationNone(stylePath);\n};\n\nconst scopeSelectorList = (selectorList: string, scopeId: string, scopeSelector: string, rootScopeSelectors: string[]): string => {\n return selectorParser((root) => {\n const sourceSelectors = [...root.nodes];\n\n for (const selector of sourceSelectors) {\n const rootNodes = selector.nodes.filter(isRootNode);\n\n if (rootNodes.length > 0) {\n const replacementSelectors = isRootOnlySelector(selector) ? rootScopeSelectors : [rootedDescendantScope(selector, scopeId)];\n\n for (const replacementSelector of replacementSelectors) {\n const variant = selector.clone();\n removeRootIdentityIds(variant);\n const [firstRoot, ...duplicateRoots] = variant.nodes.filter(isRootNode);\n // The replacement can be a compound of several nodes, e.g. :where(<scope>):not(<carrier>).\n const scopeNodes = selectorParser()\n .astSync(replacementSelector)\n .first.nodes.map((node) => node.clone());\n const [scopeNode, ...followingScopeNodes] = scopeNodes;\n if (firstRoot && scopeNode) {\n firstRoot.replaceWith(scopeNode);\n let previous = scopeNode;\n for (const node of followingScopeNodes) {\n variant.insertAfter(previous, node);\n previous = node;\n }\n }\n\n for (const duplicateRoot of duplicateRoots) {\n const previous = duplicateRoot.prev();\n if (previous?.type === 'combinator') {\n previous.remove();\n }\n duplicateRoot.remove();\n }\n\n root.insertBefore(selector, variant);\n }\n selector.remove();\n continue;\n }\n\n if (startsWithHostPseudo(selector) || isScopeNode(selector.first, scopeId)) {\n continue;\n }\n\n const first = selector.first;\n const needsBoundaryVariant = first?.type === 'universal' || (first?.type === 'pseudo' && first.value.startsWith('::'));\n\n if (needsBoundaryVariant) {\n const hasSiblingCombinator = selector.nodes.some((node) => node.type === 'combinator' && (node.value === '+' || node.value === '~'));\n\n if (!hasSiblingCombinator) {\n const boundaryVariant = selector.clone();\n const boundaryScope = selectorParser().astSync(scopeSelector).first.first.clone();\n boundaryVariant.first.rawSpaceBefore = '';\n if (boundaryVariant.first.type === 'universal') {\n boundaryVariant.first.replaceWith(boundaryScope);\n } else {\n boundaryVariant.prepend(boundaryScope);\n }\n root.insertBefore(selector, boundaryVariant);\n }\n }\n\n const descendantScope = selectorParser().astSync(`${scopeSelector} *`).first;\n const prefixNodes = descendantScope.nodes.slice(0, -1).map((node) => node.clone());\n selector.first.rawSpaceBefore = '';\n for (const prefixNode of prefixNodes.reverse()) {\n selector.prepend(prefixNode);\n }\n }\n\n const seenSelectors = new Set<string>();\n for (const selector of [...root.nodes]) {\n const normalizedSelector = selector.toString().trim();\n if (seenSelectors.has(normalizedSelector)) {\n selector.remove();\n } else {\n seenSelectors.add(normalizedSelector);\n }\n }\n }).processSync(selectorList);\n};\n\nconst namespaceKeyframes = (root: Root, keyframePrefix: string): void => {\n const keyframes = new Map<string, string>();\n\n root.walkAtRules((atRule) => {\n if (!/keyframes$/i.test(atRule.name) || atRule.params.startsWith(keyframePrefix)) {\n return;\n }\n\n const originalName = atRule.params.trim();\n const scopedName = `${keyframePrefix}${originalName}`;\n keyframes.set(originalName, scopedName);\n atRule.params = scopedName;\n });\n\n root.walkDecls((declaration) => {\n if (!/^(?:-webkit-)?animation(?:-name)?$/i.test(declaration.prop) && !declaration.prop.startsWith('--animate-')) {\n return;\n }\n\n const parsed = valueParser(declaration.value);\n parsed.walk((node) => {\n if (node.type === 'word') {\n node.value = keyframes.get(node.value) ?? node.value;\n }\n });\n declaration.value = parsed.toString();\n });\n};\n\nconst INITIAL_FONT_SIZE_PX = 16;\n// Absolute-size keywords as browsers compute them, and the relative ones as they resolve on the root, whose parent size\n// is the initial 16px.\nconst FONT_SIZE_KEYWORDS_PX: Readonly<Record<string, number>> = {\n 'xx-small': 9,\n 'x-small': 10,\n small: 13,\n medium: 16,\n large: 18,\n 'x-large': 24,\n 'xx-large': 32,\n 'xxx-large': 48,\n smaller: INITIAL_FONT_SIZE_PX / 1.2,\n larger: INITIAL_FONT_SIZE_PX * 1.2,\n};\n// On the root, every global keyword gives the initial size.\nconst GLOBAL_KEYWORDS = new Set(['inherit', 'initial', 'revert', 'revert-layer', 'unset']);\n// em, rem and % on the root are relative to the initial size.\nconst FONT_SIZE_UNITS_PX: Readonly<Record<string, number>> = {\n '%': INITIAL_FONT_SIZE_PX / 100,\n cm: 96 / 2.54,\n em: INITIAL_FONT_SIZE_PX,\n in: 96,\n mm: 96 / 25.4,\n pc: 16,\n pt: 96 / 72,\n px: 1,\n q: 96 / 101.6,\n rem: INITIAL_FONT_SIZE_PX,\n};\n\nconst fontSizeOfWord = (word: string): number | undefined => {\n const keyword = FONT_SIZE_KEYWORDS_PX[word.toLowerCase()];\n if (keyword !== undefined) {\n return keyword;\n }\n const dimension = valueParser.unit(word);\n const unitPx = dimension ? FONT_SIZE_UNITS_PX[dimension.unit.toLowerCase()] : undefined;\n const amount = Number(dimension ? dimension.number : Number.NaN);\n return unitPx !== undefined && Number.isFinite(amount) && amount >= 0 ? amount * unitPx : undefined;\n};\n\nconst significantValueNodes = (value: string): valueParser.Node[] => {\n return valueParser(value).nodes.filter((node) => node.type !== 'space' && node.type !== 'comment');\n};\n\nconst globalKeywordFontSize = (nodes: valueParser.Node[]): number | undefined => {\n const [only] = nodes;\n return nodes.length === 1 && only?.type === 'word' && GLOBAL_KEYWORDS.has(only.value.toLowerCase()) ? INITIAL_FONT_SIZE_PX : undefined;\n};\n\nconst resolveFontSize = (value: string): number | undefined => {\n const nodes = significantValueNodes(value);\n const [only] = nodes;\n return globalKeywordFontSize(nodes) ?? (nodes.length === 1 && only?.type === 'word' ? fontSizeOfWord(only.value) : undefined);\n};\n\n// `font: [style] [variant] [weight] [stretch] <size>[/<line-height>] <family>`: the size is the word before `/`, or else\n// the first word that is a size. A function (var(), calc()) or a family name reached first means it cannot be read.\nconst resolveFontShorthandSize = (value: string): number | undefined => {\n const nodes = significantValueNodes(value);\n const globalSize = globalKeywordFontSize(nodes);\n if (globalSize !== undefined) {\n return globalSize;\n }\n\n const slashIndex = nodes.findIndex((node) => node.type === 'div' && node.value === '/');\n if (slashIndex > 0) {\n const size = nodes[slashIndex - 1];\n return size?.type === 'word' ? fontSizeOfWord(size.value) : undefined;\n }\n\n for (const node of nodes) {\n if (node.type !== 'word') {\n return undefined;\n }\n const size = fontSizeOfWord(node.value);\n if (size !== undefined) {\n return size;\n }\n }\n return undefined;\n};\n\n// `html` and `:root` size the document root, so rem; `body` and `html body` size only the body.\nconst targetsDocumentRoot = (selector: selectorParser.Selector): boolean => {\n let subject: selectorParser.Node[] = [];\n for (const node of selector.nodes) {\n subject = node.type === 'combinator' ? [] : [...subject, node];\n }\n return subject.some((node) => (node.type === 'tag' && node.value.toLowerCase() === 'html') || (node.type === 'pseudo' && node.value === ':root'));\n};\n\nconst documentRootSelectorsOf = (selectorList: string): string[] => {\n const selectors: string[] = [];\n selectorParser((root) => {\n for (const selector of root.nodes) {\n if (targetsDocumentRoot(selector)) {\n selectors.push(selector.toString().trim());\n }\n }\n }).processSync(selectorList);\n return selectors;\n};\n\ntype TWarn = (node: Declaration, text: string) => void;\n\n// Hosted, rem resolves against the host page's root, not the root size the module's own page set. Each rule that sizes\n// the document root gets a sibling rule right after it that records that size as MF_ROOT_FONT_SIZE_PROPERTY, under the\n// same conditions (@media, importance) and in the same cascade order; scoping then moves it onto the module boundary,\n// where followModuleRootFontSize() makes rem lengths read it. CSS that never sizes the root is left byte-identical.\nconst recordRootFontSize = (root: Root, warn: TWarn): void => {\n root.walkRules((rule) => {\n if (isKeyframesRule(rule)) {\n return;\n }\n\n let sizing: Declaration | undefined;\n for (const node of rule.nodes) {\n if (node.type === 'decl' && /^font(?:-size)?$/i.test(node.prop) && (node.important || !sizing?.important)) {\n sizing = node;\n }\n }\n if (!sizing) {\n return;\n }\n\n const rootSelectors = documentRootSelectorsOf(rule.selector);\n if (rootSelectors.length === 0) {\n return;\n }\n\n const sizePx = sizing.prop.toLowerCase() === 'font' ? resolveFontShorthandSize(sizing.value) : resolveFontSize(sizing.value);\n if (sizePx === undefined) {\n warn(\n sizing,\n `Cannot resolve the document root font size \"${sizing.value}\" at build time; rem lengths in the hosted module will use the host page's root size.`,\n );\n return;\n }\n\n const recorded = rule.clone({ selector: rootSelectors.join(', ') });\n recorded.raws.before = rule.raws.before || '\\n';\n recorded.removeAll();\n // The recorded size points back at the declaration it came from, so source maps keep a source for it.\n recorded.append({\n important: sizing.important,\n prop: MF_ROOT_FONT_SIZE_PROPERTY,\n source: sizing.source,\n value: `${Number(sizePx.toFixed(4))}px`,\n });\n rule.after(recorded);\n });\n};\n\nconst scopeGlobalRoot = (root: Root, scopeId: string, scopeSelector: string, keyframePrefix: string, warn: TWarn): void => {\n recordRootFontSize(root, warn);\n namespaceKeyframes(root, keyframePrefix);\n const contentRootSelector = `:where([${MF_SCOPE_ATTRIBUTE}=\"${scopeId}\"]:not([${MF_OVERLAY_ATTRIBUTE}]))`;\n root.walkRules((rule) => {\n if (!isKeyframesRule(rule)) {\n const declarations = rule.nodes.filter((node) => node.type === 'decl');\n const customProperties = declarations.filter((declaration) => declaration.prop.startsWith('--'));\n const standardDeclarations = declarations.filter((declaration) => !declaration.prop.startsWith('--'));\n\n if (customProperties.length > 0 && standardDeclarations.length > 0 && hasRootOnlySelector(rule.selector)) {\n const customPropertyRule = rule.cloneBefore();\n customPropertyRule.walkDecls((declaration) => {\n if (!declaration.prop.startsWith('--')) {\n declaration.remove();\n }\n });\n customPropertyRule.selector = scopeSelectorList(customPropertyRule.selector, scopeId, scopeSelector, [':host', scopeSelector]);\n rule.walkDecls((declaration) => {\n if (declaration.prop.startsWith('--')) {\n declaration.remove();\n }\n });\n }\n\n const containsOnlyCustomProperties = declarations.length > 0 && declarations.every((declaration) => declaration.prop.startsWith('--'));\n const rootScopeSelectors = containsOnlyCustomProperties ? [':host', scopeSelector] : [':host', contentRootSelector];\n rule.selector = scopeSelectorList(rule.selector, scopeId, scopeSelector, rootScopeSelectors);\n }\n });\n};\n\n// Emulated component CSS keeps its selectors exactly as written, so Angular compiles what it compiled on the module's\n// own page and every rule keeps its weight and reach: `mat-form-field[_ngcontent-x]` (0,1,1) still loses to a global\n// `.mat-sidenav-content .mat-mdc-form-field` (0,2,0), template content rendered in an overlay (outside the component's\n// host) keeps its component styles, and a top-level `::ng-deep` rule stays global to the module. Wrapping every rule in\n// :host-context(<boundary>) added (0,2,0) to each, and limited all of them to the component's host. Isolation needs no\n// boundary here: Angular's _ngcontent and _nghost attributes are unique to each component, and a hosted module's\n// component styles live in its own shadow root.\nconst scopeAngularComponentRoot = (root: Root, keyframePrefix: string): void => {\n namespaceKeyframes(root, keyframePrefix);\n};\n\nexport const scopeRemoteCss = ({ scopeId }: IScopeRemoteCssOptions): Plugin => {\n const validScopeId = validateMfScopeId(scopeId);\n const scopeSelector = getMfScopeSelector(validScopeId);\n const keyframePrefix = `edyn-mf-${validScopeId}--`;\n\n return {\n postcssPlugin: `edyn-mf-isolation-${validScopeId}`,\n Once(root, { result }) {\n scopeGlobalRoot(root, validScopeId, scopeSelector, keyframePrefix, (node, text) => node.warn(result, text));\n },\n };\n};\n\nexport const scopeAngularRemoteCss = ({ scopeId }: IScopeRemoteCssOptions): Plugin => {\n const validScopeId = validateMfScopeId(scopeId);\n const scopeSelector = getMfScopeSelector(validScopeId);\n const keyframePrefix = `edyn-mf-${validScopeId}--`;\n\n return {\n postcssPlugin: `edyn-mf-angular-isolation-${validScopeId}`,\n Once(root, { result }) {\n if (shouldUseAngularComponentScoping(root.source?.input.file)) {\n scopeAngularComponentRoot(root, keyframePrefix);\n return;\n }\n\n scopeGlobalRoot(root, validScopeId, scopeSelector, keyframePrefix, (node, text) => node.warn(result, text));\n },\n };\n};\n"]}