openxiangda 1.0.2 → 1.0.4

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.
@@ -233,7 +233,7 @@ function createRuntimeEntryContent() {
233
233
  return `import React from 'react';
234
234
  import { createRoot } from 'react-dom/client';
235
235
  import { StyleProvider } from '@ant-design/cssinjs';
236
- import { App as AntdApp, ConfigProvider } from 'antd';
236
+ import { App as AntdApp, ConfigProvider, message } from 'antd';
237
237
  import zhCN from 'antd/locale/zh_CN';
238
238
  import * as SyFormComponentsModule from 'openxiangda';
239
239
  import { antdTheme } from 'openxiangda/antd-theme';
@@ -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,73 @@ 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) {
339
+ const getMessageContainer = () =>
340
+ portalContainer?.isConnected ? portalContainer : getRuntimeRoot(el);
341
+
315
342
  ConfigProvider.config({
316
343
  prefixCls: 'sy-ant',
317
344
  iconPrefixCls: 'sy-anticon',
@@ -326,27 +353,37 @@ function installAntdStaticHolder(el) {
326
353
  }
327
354
  return (
328
355
  <StyleProvider hashPriority="high" container={getStyleContainer(el)}>
329
- <ConfigProvider {...createAntdConfig(el)}>{children}</ConfigProvider>
356
+ <ConfigProvider {...createAntdConfig(el, portalContainer)}>{children}</ConfigProvider>
330
357
  </StyleProvider>
331
358
  );
332
359
  },
333
360
  });
361
+ message.config({
362
+ prefixCls: 'sy-ant-message',
363
+ getContainer: getMessageContainer,
364
+ });
334
365
  }
335
366
 
336
367
  function renderStandardForm(el, schemaInput, context = {}) {
337
368
  let root = roots.get(el);
338
369
  el.classList.add(NAMESPACE_ROOT_CLASS);
339
- if (!portalContainerCleanups.has(el)) {
340
- portalContainerCleanups.set(el, installRuntimePortalContainer(el));
370
+ let portalContainer = portalContainers.get(el);
371
+ if (!portalContainer?.isConnected) {
372
+ const cleanup = portalContainerCleanups.get(el);
373
+ if (cleanup) cleanup();
374
+ const portalHandle = installRuntimePortalContainer(el);
375
+ portalContainer = portalHandle.container;
376
+ portalContainers.set(el, portalContainer);
377
+ portalContainerCleanups.set(el, portalHandle.release);
341
378
  }
342
- installAntdStaticHolder(el);
379
+ installAntdStaticHolder(el, portalContainer);
343
380
  if (!root) {
344
381
  root = createRoot(el);
345
382
  roots.set(el, root);
346
383
  }
347
384
 
348
385
  const schema = defineFormSchema(schemaInput);
349
- const antdConfig = createAntdConfig(el);
386
+ const antdConfig = createAntdConfig(el, portalContainer);
350
387
  root.render(
351
388
  <StyleProvider hashPriority="high" container={getStyleContainer(el)}>
352
389
  <ConfigProvider {...antdConfig}>
@@ -380,6 +417,7 @@ function unmountStandardForm(el) {
380
417
  if (releasePortalContainer) {
381
418
  releasePortalContainer();
382
419
  portalContainerCleanups.delete(el);
420
+ portalContainers.delete(el);
383
421
  }
384
422
  }
385
423
 
@@ -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
  };