openxiangda 1.0.1 → 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.
@@ -30,6 +30,7 @@ const runtimeDistDir = path.join(rootDir, "dist/form-runtime");
30
30
  const tmpDir = path.join(rootDir, ".tmp");
31
31
  const runtimeVersionPlaceholder = "__SY_FORM_RUNTIME_VERSION__";
32
32
  const runtimeCacheFileName = "build-cache.json";
33
+ const portalContainerResolverGlobal = "__OPENXIANGDA_GET_PORTAL_CONTAINER__";
33
34
 
34
35
  const runtimePackages = [
35
36
  "react",
@@ -164,7 +165,6 @@ function parseArgs(argv) {
164
165
  }
165
166
  if (arg === "--force") {
166
167
  result.force = true;
167
- result.runtimeCache = false;
168
168
  continue;
169
169
  }
170
170
  if (arg === "--dry-run") {
@@ -196,7 +196,7 @@ build-forms - 构建表单页面
196
196
  选项:
197
197
  --form <name> 只构建指定表单(src/forms/ 下的目录名)
198
198
  --only <list> 只构建指定模块,如 forms/customer,pages/dashboard
199
- --force 忽略增量缓存,强制重建
199
+ --force 忽略增量缓存,强制重建表单产物
200
200
  --dry-run 只打印增量计划,不构建
201
201
  --clean-cache 删除 .openxiangda/build-cache.json
202
202
  --no-runtime-cache 强制重建共享 runtime
@@ -242,7 +242,13 @@ import '../src/index.css';
242
242
 
243
243
  const runtimeVersion = '${runtimeVersionPlaceholder}';
244
244
  const roots = new WeakMap();
245
+ const portalContainerCleanups = new WeakMap();
245
246
  const { StandardFormPage, defineFormSchema } = SyFormComponentsModule;
247
+ const NAMESPACE_ROOT_CLASS = 'sy-app-workspace';
248
+ const RUNTIME_PORTAL_ATTR = 'data-sy-runtime-portal';
249
+ const PORTAL_CONTAINER_STACK_GLOBAL = '__OPENXIANGDA_PORTAL_CONTAINER_STACK__';
250
+ const PORTAL_CONTAINER_RESOLVER_GLOBAL = '${portalContainerResolverGlobal}';
251
+ const portalContainers = new WeakMap();
246
252
 
247
253
  function getStyleContainer(el) {
248
254
  const rootNode = el.getRootNode?.();
@@ -251,58 +257,143 @@ function getStyleContainer(el) {
251
257
  : document.head;
252
258
  }
253
259
 
254
- function getNamespaceRoot(el) {
255
- return el.closest?.('.sy-app-workspace') || el;
260
+ function isShadowRoot(rootNode) {
261
+ return typeof ShadowRoot !== 'undefined' && rootNode instanceof ShadowRoot;
256
262
  }
257
263
 
258
- function isInShadowRoot(el) {
259
- const rootNode = el.getRootNode?.();
260
- return typeof ShadowRoot !== 'undefined' && rootNode instanceof ShadowRoot;
264
+ function getRuntimeRoot(el, triggerNode) {
265
+ if (isShadowRoot(el.getRootNode?.())) return el;
266
+ const triggerRoot = triggerNode?.closest?.('.' + NAMESPACE_ROOT_CLASS);
267
+ return (
268
+ triggerRoot ||
269
+ el.closest?.('.' + NAMESPACE_ROOT_CLASS) ||
270
+ el.querySelector?.('.' + NAMESPACE_ROOT_CLASS) ||
271
+ el
272
+ );
261
273
  }
262
274
 
263
- function getPopupContainer(el) {
264
- const inShadowRoot = isInShadowRoot(el);
265
- return (triggerNode) => {
266
- if (inShadowRoot) return el;
267
- return getNamespaceRoot(el) || triggerNode?.parentElement || document.body;
268
- };
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);
269
281
  }
270
282
 
271
283
  function getTargetContainer(el) {
272
- return () => (isInShadowRoot(el) ? el : getNamespaceRoot(el) || document.body);
284
+ return () => getRuntimeRoot(el);
285
+ }
286
+
287
+ function createAntdConfig(el, portalContainer) {
288
+ return {
289
+ locale: zhCN,
290
+ prefixCls: 'sy-ant',
291
+ iconPrefixCls: 'sy-anticon',
292
+ theme: antdTheme,
293
+ getPopupContainer: getPopupContainer(el, portalContainer),
294
+ getTargetContainer: getTargetContainer(el),
295
+ };
296
+ }
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
+
308
+ function installRuntimePortalContainer(el) {
309
+ const portalContainer = createPortalContainer(el);
310
+ const stack = Array.isArray(globalThis[PORTAL_CONTAINER_STACK_GLOBAL])
311
+ ? globalThis[PORTAL_CONTAINER_STACK_GLOBAL]
312
+ : [];
313
+ stack.push(portalContainer);
314
+ globalThis[PORTAL_CONTAINER_STACK_GLOBAL] = stack;
315
+ globalThis[PORTAL_CONTAINER_RESOLVER_GLOBAL] = () => {
316
+ for (let index = stack.length - 1; index >= 0; index -= 1) {
317
+ const candidate = stack[index];
318
+ if (candidate?.isConnected) {
319
+ return candidate;
320
+ }
321
+ }
322
+ return (
323
+ document.querySelector('[' + RUNTIME_PORTAL_ATTR + ']') ||
324
+ document.querySelector('.' + NAMESPACE_ROOT_CLASS) ||
325
+ document.body
326
+ );
327
+ };
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
+ },
335
+ };
336
+ }
337
+
338
+ function installAntdStaticHolder(el, portalContainer) {
339
+ ConfigProvider.config({
340
+ prefixCls: 'sy-ant',
341
+ iconPrefixCls: 'sy-anticon',
342
+ theme: antdTheme,
343
+ holderRender: (children) => {
344
+ if (!el.isConnected) {
345
+ return (
346
+ <ConfigProvider prefixCls="sy-ant" iconPrefixCls="sy-anticon" theme={antdTheme}>
347
+ {children}
348
+ </ConfigProvider>
349
+ );
350
+ }
351
+ return (
352
+ <StyleProvider hashPriority="high" container={getStyleContainer(el)}>
353
+ <ConfigProvider {...createAntdConfig(el, portalContainer)}>{children}</ConfigProvider>
354
+ </StyleProvider>
355
+ );
356
+ },
357
+ });
273
358
  }
274
359
 
275
360
  function renderStandardForm(el, schemaInput, context = {}) {
276
361
  let root = roots.get(el);
362
+ el.classList.add(NAMESPACE_ROOT_CLASS);
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);
371
+ }
372
+ installAntdStaticHolder(el, portalContainer);
277
373
  if (!root) {
278
- if (!isInShadowRoot(el)) el.classList.add('sy-app-workspace');
279
374
  root = createRoot(el);
280
375
  roots.set(el, root);
281
376
  }
282
377
 
283
378
  const schema = defineFormSchema(schemaInput);
379
+ const antdConfig = createAntdConfig(el, portalContainer);
284
380
  root.render(
285
381
  <StyleProvider hashPriority="high" container={getStyleContainer(el)}>
286
- <ConfigProvider
287
- locale={zhCN}
288
- prefixCls="sy-ant"
289
- iconPrefixCls="sy-anticon"
290
- theme={antdTheme}
291
- getPopupContainer={getPopupContainer(el)}
292
- getTargetContainer={getTargetContainer(el)}
293
- >
382
+ <ConfigProvider {...antdConfig}>
294
383
  <AntdApp>
295
- <StandardFormPage
296
- schema={schema}
297
- mode={context.mode || 'submit'}
298
- initialValues={context.initialValues}
299
- permissions={context.permissions}
300
- formUuid={context.formUuid}
301
- appType={context.appType}
302
- formInstanceId={context.formInstanceId}
303
- onSubmit={context.onSubmit}
304
- inDrawer={context.inDrawer}
305
- />
384
+ <div className="sy-app-workspace">
385
+ <StandardFormPage
386
+ schema={schema}
387
+ mode={context.mode || 'submit'}
388
+ initialValues={context.initialValues}
389
+ permissions={context.permissions}
390
+ formUuid={context.formUuid}
391
+ appType={context.appType}
392
+ formInstanceId={context.formInstanceId}
393
+ onSubmit={context.onSubmit}
394
+ inDrawer={context.inDrawer}
395
+ />
396
+ </div>
306
397
  </AntdApp>
307
398
  </ConfigProvider>
308
399
  </StyleProvider>
@@ -315,6 +406,12 @@ function unmountStandardForm(el) {
315
406
  root.unmount();
316
407
  roots.delete(el);
317
408
  }
409
+ const releasePortalContainer = portalContainerCleanups.get(el);
410
+ if (releasePortalContainer) {
411
+ releasePortalContainer();
412
+ portalContainerCleanups.delete(el);
413
+ portalContainers.delete(el);
414
+ }
318
415
  }
319
416
 
320
417
  export function createStandardFormModule(schemaInput) {
@@ -356,6 +453,24 @@ export default formRuntime;
356
453
  `;
357
454
  }
358
455
 
456
+ function createAntdMobilePortalPatchPlugin() {
457
+ const resolverExpression = `globalThis.${portalContainerResolverGlobal}?.() || document.body`;
458
+ return {
459
+ name: "openxiangda-antd-mobile-portal-container",
460
+ transform(code, id) {
461
+ if (!id.includes("/antd-mobile/") && !id.includes("\\\\antd-mobile\\\\")) {
462
+ return null;
463
+ }
464
+ const nextCode = code.replace(
465
+ /getContainer:\s*\(\)\s*=>\s*document\.body/g,
466
+ `getContainer: () => ${resolverExpression}`,
467
+ );
468
+ if (nextCode === code) return null;
469
+ return { code: nextCode, map: null };
470
+ },
471
+ };
472
+ }
473
+
359
474
  function createFormRuntimeProxyPlugin() {
360
475
  return {
361
476
  name: "sy-form-runtime-proxy",
@@ -547,7 +662,7 @@ async function buildSharedRuntime(options = {}) {
547
662
  },
548
663
  },
549
664
  },
550
- plugins: [reactPlugin],
665
+ plugins: [createAntdMobilePortalPatchPlugin(), reactPlugin],
551
666
  logLevel: "warn",
552
667
  });
553
668
 
@@ -689,7 +804,11 @@ async function buildForm(form) {
689
804
  },
690
805
  },
691
806
  },
692
- plugins: [createFormRuntimeProxyPlugin(), reactPlugin].filter(Boolean),
807
+ plugins: [
808
+ createAntdMobilePortalPatchPlugin(),
809
+ createFormRuntimeProxyPlugin(),
810
+ reactPlugin,
811
+ ].filter(Boolean),
693
812
  logLevel: "warn",
694
813
  });
695
814
 
@@ -34,12 +34,14 @@ const tmpDir = path.join(rootDir, ".tmp");
34
34
  const runtimeDistDir = path.join(distRoot, "page-runtime");
35
35
  const runtimeVersionPlaceholder = "__SY_PAGE_RUNTIME_VERSION__";
36
36
  const runtimeCacheFileName = "build-cache.json";
37
+ const portalContainerResolverGlobal = "__OPENXIANGDA_GET_PORTAL_CONTAINER__";
37
38
 
38
39
  const runtimePackages = [
39
40
  "react",
40
41
  "react-dom",
41
42
  "@ant-design/cssinjs",
42
43
  "antd",
44
+ "antd-mobile",
43
45
  "@ant-design/icons",
44
46
  "openxiangda",
45
47
  ];
@@ -163,7 +165,6 @@ function parseArgs(argv) {
163
165
  }
164
166
  if (arg === "--force") {
165
167
  result.force = true;
166
- result.runtimeCache = false;
167
168
  continue;
168
169
  }
169
170
  if (arg === "--dry-run") {
@@ -194,7 +195,7 @@ build-pages - 构建复杂代码页
194
195
  选项:
195
196
  --page <name> 只构建指定页面目录
196
197
  --only <list> 只构建指定模块,如 forms/customer,pages/dashboard
197
- --force 忽略增量缓存,强制重建
198
+ --force 忽略增量缓存,强制重建页面产物
198
199
  --dry-run 只打印增量计划,不构建
199
200
  --clean-cache 删除 .openxiangda/build-cache.json
200
201
  --no-runtime-cache 强制重建共享 runtime
@@ -339,6 +340,24 @@ async function createCssOptions() {
339
340
  };
340
341
  }
341
342
 
343
+ function createAntdMobilePortalPatchPlugin() {
344
+ const resolverExpression = `globalThis.${portalContainerResolverGlobal}?.() || document.body`;
345
+ return {
346
+ name: "openxiangda-antd-mobile-portal-container",
347
+ transform(code, id) {
348
+ if (!id.includes("/antd-mobile/") && !id.includes("\\\\antd-mobile\\\\")) {
349
+ return null;
350
+ }
351
+ const nextCode = code.replace(
352
+ /getContainer:\s*\(\)\s*=>\s*document\.body/g,
353
+ `getContainer: () => ${resolverExpression}`,
354
+ );
355
+ if (nextCode === code) return null;
356
+ return { code: nextCode, map: null };
357
+ },
358
+ };
359
+ }
360
+
342
361
  function createResolveAlias() {
343
362
  return [
344
363
  {
@@ -373,7 +392,7 @@ async function buildSharedRuntime(options = {}) {
373
392
  "process.env.NODE_ENV": JSON.stringify("production"),
374
393
  "process.env": JSON.stringify({ NODE_ENV: "production" }),
375
394
  },
376
- plugins: [react()],
395
+ plugins: [createAntdMobilePortalPatchPlugin(), react()],
377
396
  resolve: {
378
397
  alias: createResolveAlias(),
379
398
  dedupe: ["react", "react-dom", "antd", "@ant-design/cssinjs"],
@@ -655,7 +674,11 @@ async function buildPage(page) {
655
674
  "process.env.NODE_ENV": JSON.stringify("production"),
656
675
  "process.env": JSON.stringify({ NODE_ENV: "production" }),
657
676
  },
658
- plugins: [createSharedRuntimeProxyPlugin(), react()],
677
+ plugins: [
678
+ createAntdMobilePortalPatchPlugin(),
679
+ createSharedRuntimeProxyPlugin(),
680
+ react(),
681
+ ],
659
682
  resolve: {
660
683
  alias: createResolveAlias(),
661
684
  dedupe: ["react", "react-dom", "antd", "@ant-design/cssinjs"],
@@ -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
  };