openxiangda 1.0.2 → 1.0.3

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.
@@ -245,8 +245,10 @@ const roots = new WeakMap();
245
245
  const portalContainerCleanups = new WeakMap();
246
246
  const { StandardFormPage, defineFormSchema } = SyFormComponentsModule;
247
247
  const NAMESPACE_ROOT_CLASS = 'sy-app-workspace';
248
+ const RUNTIME_PORTAL_ATTR = 'data-sy-runtime-portal';
248
249
  const PORTAL_CONTAINER_STACK_GLOBAL = '__OPENXIANGDA_PORTAL_CONTAINER_STACK__';
249
250
  const PORTAL_CONTAINER_RESOLVER_GLOBAL = '${portalContainerResolverGlobal}';
251
+ const portalContainers = new WeakMap();
250
252
 
251
253
  function getStyleContainer(el) {
252
254
  const rootNode = el.getRootNode?.();
@@ -255,13 +257,12 @@ function getStyleContainer(el) {
255
257
  : document.head;
256
258
  }
257
259
 
258
- function isInShadowRoot(el) {
259
- const rootNode = el.getRootNode?.();
260
+ function isShadowRoot(rootNode) {
260
261
  return typeof ShadowRoot !== 'undefined' && rootNode instanceof ShadowRoot;
261
262
  }
262
263
 
263
- function getNamespaceRoot(el, triggerNode) {
264
- if (isInShadowRoot(el)) return el;
264
+ function getRuntimeRoot(el, triggerNode) {
265
+ if (isShadowRoot(el.getRootNode?.())) return el;
265
266
  const triggerRoot = triggerNode?.closest?.('.' + NAMESPACE_ROOT_CLASS);
266
267
  return (
267
268
  triggerRoot ||
@@ -271,47 +272,70 @@ function getNamespaceRoot(el, triggerNode) {
271
272
  );
272
273
  }
273
274
 
274
- function getPopupContainer(el) {
275
- return (triggerNode) => getNamespaceRoot(el, triggerNode);
275
+ function getOverlayRoot(el, portalContainer, triggerNode) {
276
+ return portalContainer?.isConnected ? portalContainer : getRuntimeRoot(el, triggerNode);
277
+ }
278
+
279
+ function getPopupContainer(el, portalContainer) {
280
+ return (triggerNode) => getOverlayRoot(el, portalContainer, triggerNode);
276
281
  }
277
282
 
278
283
  function getTargetContainer(el) {
279
- return () => getNamespaceRoot(el);
284
+ return () => getRuntimeRoot(el);
280
285
  }
281
286
 
282
- function createAntdConfig(el) {
287
+ function createAntdConfig(el, portalContainer) {
283
288
  return {
284
289
  locale: zhCN,
285
290
  prefixCls: 'sy-ant',
286
291
  iconPrefixCls: 'sy-anticon',
287
292
  theme: antdTheme,
288
- getPopupContainer: getPopupContainer(el),
293
+ getPopupContainer: getPopupContainer(el, portalContainer),
289
294
  getTargetContainer: getTargetContainer(el),
290
295
  };
291
296
  }
292
297
 
298
+ function createPortalContainer(el) {
299
+ const rootNode = el.getRootNode?.();
300
+ const parent = isShadowRoot(rootNode) ? rootNode : el.ownerDocument.body;
301
+ const portalContainer = el.ownerDocument.createElement('div');
302
+ portalContainer.setAttribute(RUNTIME_PORTAL_ATTR, '');
303
+ portalContainer.classList.add(NAMESPACE_ROOT_CLASS);
304
+ parent.appendChild(portalContainer);
305
+ return portalContainer;
306
+ }
307
+
293
308
  function installRuntimePortalContainer(el) {
309
+ const portalContainer = createPortalContainer(el);
294
310
  const stack = Array.isArray(globalThis[PORTAL_CONTAINER_STACK_GLOBAL])
295
311
  ? globalThis[PORTAL_CONTAINER_STACK_GLOBAL]
296
312
  : [];
297
- stack.push(el);
313
+ stack.push(portalContainer);
298
314
  globalThis[PORTAL_CONTAINER_STACK_GLOBAL] = stack;
299
315
  globalThis[PORTAL_CONTAINER_RESOLVER_GLOBAL] = () => {
300
316
  for (let index = stack.length - 1; index >= 0; index -= 1) {
301
317
  const candidate = stack[index];
302
318
  if (candidate?.isConnected) {
303
- return getNamespaceRoot(candidate);
319
+ return candidate;
304
320
  }
305
321
  }
306
- return document.querySelector('.' + NAMESPACE_ROOT_CLASS) || document.body;
322
+ return (
323
+ document.querySelector('[' + RUNTIME_PORTAL_ATTR + ']') ||
324
+ document.querySelector('.' + NAMESPACE_ROOT_CLASS) ||
325
+ document.body
326
+ );
307
327
  };
308
- return () => {
309
- const position = stack.lastIndexOf(el);
310
- if (position >= 0) stack.splice(position, 1);
328
+ return {
329
+ container: portalContainer,
330
+ release: () => {
331
+ const position = stack.lastIndexOf(portalContainer);
332
+ if (position >= 0) stack.splice(position, 1);
333
+ portalContainer.remove();
334
+ },
311
335
  };
312
336
  }
313
337
 
314
- function installAntdStaticHolder(el) {
338
+ function installAntdStaticHolder(el, portalContainer) {
315
339
  ConfigProvider.config({
316
340
  prefixCls: 'sy-ant',
317
341
  iconPrefixCls: 'sy-anticon',
@@ -326,7 +350,7 @@ function installAntdStaticHolder(el) {
326
350
  }
327
351
  return (
328
352
  <StyleProvider hashPriority="high" container={getStyleContainer(el)}>
329
- <ConfigProvider {...createAntdConfig(el)}>{children}</ConfigProvider>
353
+ <ConfigProvider {...createAntdConfig(el, portalContainer)}>{children}</ConfigProvider>
330
354
  </StyleProvider>
331
355
  );
332
356
  },
@@ -336,17 +360,23 @@ function installAntdStaticHolder(el) {
336
360
  function renderStandardForm(el, schemaInput, context = {}) {
337
361
  let root = roots.get(el);
338
362
  el.classList.add(NAMESPACE_ROOT_CLASS);
339
- if (!portalContainerCleanups.has(el)) {
340
- portalContainerCleanups.set(el, installRuntimePortalContainer(el));
363
+ let portalContainer = portalContainers.get(el);
364
+ if (!portalContainer?.isConnected) {
365
+ const cleanup = portalContainerCleanups.get(el);
366
+ if (cleanup) cleanup();
367
+ const portalHandle = installRuntimePortalContainer(el);
368
+ portalContainer = portalHandle.container;
369
+ portalContainers.set(el, portalContainer);
370
+ portalContainerCleanups.set(el, portalHandle.release);
341
371
  }
342
- installAntdStaticHolder(el);
372
+ installAntdStaticHolder(el, portalContainer);
343
373
  if (!root) {
344
374
  root = createRoot(el);
345
375
  roots.set(el, root);
346
376
  }
347
377
 
348
378
  const schema = defineFormSchema(schemaInput);
349
- const antdConfig = createAntdConfig(el);
379
+ const antdConfig = createAntdConfig(el, portalContainer);
350
380
  root.render(
351
381
  <StyleProvider hashPriority="high" container={getStyleContainer(el)}>
352
382
  <ConfigProvider {...antdConfig}>
@@ -380,6 +410,7 @@ function unmountStandardForm(el) {
380
410
  if (releasePortalContainer) {
381
411
  releasePortalContainer();
382
412
  portalContainerCleanups.delete(el);
413
+ portalContainers.delete(el);
383
414
  }
384
415
  }
385
416
 
@@ -37,6 +37,18 @@ function shouldSkipSelector(selector, prefix) {
37
37
  return false;
38
38
  }
39
39
 
40
+ function createAntdPrefixAlias(selector) {
41
+ const aliased = selector
42
+ .replace(/(^|[^A-Za-z0-9_-])\.ant-/g, "$1.sy-ant-")
43
+ .replace(/(^|[^A-Za-z0-9_-])\.anticon-/g, "$1.sy-anticon-")
44
+ .replace(/(^|[^A-Za-z0-9_-])\.anticon(?=$|[^A-Za-z0-9_-])/g, "$1.sy-anticon");
45
+ return aliased === selector ? null : aliased;
46
+ }
47
+
48
+ function uniqueSelectors(selectors) {
49
+ return Array.from(new Set(selectors));
50
+ }
51
+
40
52
  export function createNamespaceCssPlugin(prefix = DEFAULT_PREFIX) {
41
53
  return {
42
54
  postcssPlugin: "openxiangda-namespace-css",
@@ -49,11 +61,15 @@ export function createNamespaceCssPlugin(prefix = DEFAULT_PREFIX) {
49
61
  }
50
62
  parent = parent.parent;
51
63
  }
52
- rule.selector = splitSelectors(rule.selector)
53
- .map((selector) =>
54
- shouldSkipSelector(selector, prefix) ? selector : `${prefix} ${selector}`,
55
- )
56
- .join(", ");
64
+ rule.selector = uniqueSelectors(
65
+ splitSelectors(rule.selector).flatMap((selector) => {
66
+ const scopedSelector = shouldSkipSelector(selector, prefix)
67
+ ? selector
68
+ : `${prefix} ${selector}`;
69
+ const antdAlias = createAntdPrefixAlias(scopedSelector);
70
+ return antdAlias ? [scopedSelector, antdAlias] : [scopedSelector];
71
+ }),
72
+ ).join(", ");
57
73
  },
58
74
  };
59
75
  }
@@ -0,0 +1,29 @@
1
+ import postcss from "postcss";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { createNamespaceCssPlugin } from "./namespace-css.mjs";
5
+
6
+ function transform(css: string) {
7
+ return postcss([createNamespaceCssPlugin()]).process(css, {
8
+ from: undefined,
9
+ }).css;
10
+ }
11
+
12
+ describe("createNamespaceCssPlugin", () => {
13
+ it("adds sy-ant aliases for developer CSS that targets default antd classes", () => {
14
+ const css = transform(
15
+ ".portal-search .ant-input-affix-wrapper,.ant-select-dropdown .ant-select-item,.anticon-close{background:#fff}",
16
+ );
17
+
18
+ expect(css).toContain(
19
+ ".sy-app-workspace .portal-search .ant-input-affix-wrapper",
20
+ );
21
+ expect(css).toContain(
22
+ ".sy-app-workspace .portal-search .sy-ant-input-affix-wrapper",
23
+ );
24
+ expect(css).toContain(".ant-select-dropdown .ant-select-item");
25
+ expect(css).toContain(".sy-ant-select-dropdown .sy-ant-select-item");
26
+ expect(css).toContain(".anticon-close");
27
+ expect(css).toContain(".sy-anticon-close");
28
+ });
29
+ });
@@ -29,6 +29,18 @@ const layeredTailwindCss = `@import "openxiangda/styles/tokens.css";
29
29
  @tailwind components;
30
30
  @tailwind utilities;
31
31
  `;
32
+ const canonicalPostcssConfig = `const tailwindcss = require("tailwindcss");
33
+ const autoprefixer = require("autoprefixer");
34
+ const { createOpenXiangdaNamespaceCssPlugin } = require("openxiangda/build");
35
+
36
+ module.exports = {
37
+ plugins: [
38
+ tailwindcss(),
39
+ createOpenXiangdaNamespaceCssPlugin(),
40
+ autoprefixer(),
41
+ ],
42
+ };
43
+ `;
32
44
 
33
45
  export const canonicalTailwindConfig = `${managedHeader}
34
46
  /** @type {import('tailwindcss').Config} */
@@ -71,6 +83,15 @@ export function isWorkspaceTailwindCssCurrent(content) {
71
83
  );
72
84
  }
73
85
 
86
+ export function isWorkspacePostcssConfigCurrent(content) {
87
+ return Boolean(
88
+ content.includes("createOpenXiangdaNamespaceCssPlugin") &&
89
+ content.includes("openxiangda/build") &&
90
+ content.includes("tailwindcss") &&
91
+ content.includes("autoprefixer"),
92
+ );
93
+ }
94
+
74
95
  function hasBlocklistEntry(content, entry) {
75
96
  const singleQuoted = `'${entry.slice(1, -1)}'`;
76
97
  return content.includes(entry) || content.includes(singleQuoted);
@@ -176,6 +197,22 @@ export function patchWorkspaceTailwindCss(content) {
176
197
  return customCss ? `${layeredTailwindCss}\n${customCss}` : layeredTailwindCss;
177
198
  }
178
199
 
200
+ function isStandardPostcssConfig(content) {
201
+ return (
202
+ /plugins\s*:\s*\{[\s\S]*tailwindcss\s*:\s*\{[\s\S]*autoprefixer\s*:\s*\{[\s\S]*\}/s.test(
203
+ content,
204
+ ) || /plugins\s*:\s*\[[\s\S]*tailwindcss\([\s\S]*autoprefixer\(/s.test(content)
205
+ );
206
+ }
207
+
208
+ export function patchWorkspacePostcssConfig(content) {
209
+ if (isWorkspacePostcssConfigCurrent(content)) return content;
210
+ if (!content.trim() || isStandardPostcssConfig(content)) {
211
+ return canonicalPostcssConfig;
212
+ }
213
+ return content;
214
+ }
215
+
179
216
  export function ensureWorkspaceTailwindConfig(workspaceRoot) {
180
217
  const configPath = path.join(workspaceRoot, "tailwind.config.cjs");
181
218
  const current = fs.existsSync(configPath)
@@ -196,10 +233,23 @@ export function ensureWorkspaceTailwindConfig(workspaceRoot) {
196
233
  fs.writeFileSync(cssPath, nextCss, "utf-8");
197
234
  }
198
235
 
236
+ const postcssPath = path.join(workspaceRoot, "postcss.config.cjs");
237
+ const currentPostcss = fs.existsSync(postcssPath)
238
+ ? fs.readFileSync(postcssPath, "utf-8")
239
+ : "";
240
+ const nextPostcss = patchWorkspacePostcssConfig(currentPostcss);
241
+ if (currentPostcss !== nextPostcss) {
242
+ fs.writeFileSync(postcssPath, nextPostcss, "utf-8");
243
+ }
244
+
199
245
  return {
200
- changed: current !== nextContent || currentCss !== nextCss,
246
+ changed:
247
+ current !== nextContent ||
248
+ currentCss !== nextCss ||
249
+ currentPostcss !== nextPostcss,
201
250
  path: configPath,
202
251
  cssPath,
252
+ postcssPath,
203
253
  };
204
254
  }
205
255
 
@@ -6,6 +6,7 @@ import { afterEach, describe, expect, it } from "vitest";
6
6
 
7
7
  import {
8
8
  ensureWorkspaceTailwindConfig,
9
+ patchWorkspacePostcssConfig,
9
10
  patchWorkspaceTailwindCss,
10
11
  patchWorkspaceTailwindConfig,
11
12
  validateWorkspaceTailwindConfig,
@@ -42,6 +43,10 @@ describe("workspace Tailwind config helpers", () => {
42
43
  path.join(workspaceRoot, "src", "index.css"),
43
44
  "utf-8",
44
45
  );
46
+ const postcssContent = fs.readFileSync(
47
+ path.join(workspaceRoot, "postcss.config.cjs"),
48
+ "utf-8",
49
+ );
45
50
 
46
51
  expect(result.changed).toBe(true);
47
52
  expect(nextContent).toContain(
@@ -53,6 +58,8 @@ describe("workspace Tailwind config helpers", () => {
53
58
  expect(cssContent).toContain("@tailwind base;");
54
59
  expect(cssContent).toContain("@tailwind components;");
55
60
  expect(cssContent).toContain("@tailwind utilities;");
61
+ expect(postcssContent).toContain("createOpenXiangdaNamespaceCssPlugin");
62
+ expect(postcssContent).toContain('require("openxiangda/build")');
56
63
  expect(validateWorkspaceTailwindConfig(workspaceRoot)).toEqual([]);
57
64
  });
58
65
 
@@ -115,7 +122,7 @@ module.exports = {
115
122
  }
116
123
  `);
117
124
 
118
- expect(nextContent.startsWith("@layer tailwind-base {")).toBe(true);
125
+ expect(nextContent).toContain("@layer tailwind-base {");
119
126
  expect(nextContent).toContain("@tailwind components;");
120
127
  expect(nextContent).toContain("#root");
121
128
  });
@@ -136,6 +143,28 @@ module.exports = {
136
143
  expect(patchWorkspaceTailwindCss(source)).toBe(source);
137
144
  });
138
145
 
146
+ it("patches standard PostCSS config with openxiangda namespace css plugin", () => {
147
+ const nextContent = patchWorkspacePostcssConfig(`module.exports = {
148
+ plugins: {
149
+ tailwindcss: {},
150
+ autoprefixer: {},
151
+ },
152
+ };
153
+ `);
154
+
155
+ expect(nextContent).toContain("createOpenXiangdaNamespaceCssPlugin");
156
+ expect(nextContent).toContain("tailwindcss()");
157
+ expect(nextContent).toContain("autoprefixer()");
158
+ });
159
+
160
+ it("preserves custom PostCSS config when it cannot be patched safely", () => {
161
+ const source = `const custom = require("./postcss-custom");
162
+ module.exports = { plugins: [custom()] };
163
+ `;
164
+
165
+ expect(patchWorkspacePostcssConfig(source)).toBe(source);
166
+ });
167
+
139
168
  it("removes stale antd layer declaration from index css", () => {
140
169
  const nextContent = patchWorkspaceTailwindCss(`@layer tailwind-base, antd;
141
170
 
@@ -148,7 +177,7 @@ module.exports = {
148
177
  `);
149
178
 
150
179
  expect(nextContent).not.toContain("@layer tailwind-base, antd;");
151
- expect(nextContent.startsWith("@layer tailwind-base {")).toBe(true);
180
+ expect(nextContent).toContain("@layer tailwind-base {");
152
181
  });
153
182
 
154
183
  it("reports stale config during update check", () => {
@@ -1,6 +1,24 @@
1
+ const tailwindcss = require("tailwindcss");
2
+ const autoprefixer = require("autoprefixer");
3
+
4
+ function loadOpenXiangdaBuild() {
5
+ try {
6
+ return require("openxiangda/build");
7
+ } catch (error) {
8
+ try {
9
+ return require("../../packages/sdk/dist/build/index.cjs");
10
+ } catch {
11
+ throw error;
12
+ }
13
+ }
14
+ }
15
+
16
+ const { createOpenXiangdaNamespaceCssPlugin } = loadOpenXiangdaBuild();
17
+
1
18
  module.exports = {
2
- plugins: {
3
- tailwindcss: {},
4
- autoprefixer: {},
5
- },
19
+ plugins: [
20
+ tailwindcss(),
21
+ createOpenXiangdaNamespaceCssPlugin(),
22
+ autoprefixer(),
23
+ ],
6
24
  };