openxiangda 1.0.15 → 1.0.16

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -19,6 +19,10 @@ import {
19
19
  } from "./utils/incremental.mjs";
20
20
  import { createNamespaceCssPlugin } from "./utils/namespace-css.mjs";
21
21
  import { validateRuntimeCssFile } from "./utils/runtime-css-check.mjs";
22
+ import {
23
+ extractLargeDataUrlAssets,
24
+ formatExtractedAssetSummary,
25
+ } from "./utils/static-assets.mjs";
22
26
 
23
27
  process.env.NODE_ENV = "production";
24
28
  process.env.BABEL_ENV = "production";
@@ -728,6 +732,12 @@ async function buildSharedRuntime(options = {}) {
728
732
 
729
733
  const runtimeJsPath = path.join(runtimeDistDir, "runtime.js");
730
734
  const runtimeCssPath = path.join(runtimeDistDir, "style.css");
735
+ const extractedAssets = extractLargeDataUrlAssets({
736
+ outDir: runtimeDistDir,
737
+ files: [runtimeJsPath, runtimeCssPath],
738
+ });
739
+ const extractedSummary = formatExtractedAssetSummary(extractedAssets);
740
+ if (extractedSummary) console.log(`[build] ${extractedSummary}`);
731
741
  validateRuntimeCssFile(runtimeCssPath, { label: "表单共享 runtime" });
732
742
  const runtimeOutputCode = fs.readFileSync(runtimeJsPath, "utf-8");
733
743
  if (/\bjsxDEV\b|react\/jsx-dev-runtime/.test(runtimeOutputCode)) {
@@ -877,6 +887,12 @@ async function buildForm(form) {
877
887
  if (!fs.existsSync(cssOutput)) {
878
888
  fs.writeFileSync(cssOutput, "", "utf-8");
879
889
  }
890
+ const extractedAssets = extractLargeDataUrlAssets({
891
+ outDir: form.outDir,
892
+ files: [entryOutput, cssOutput],
893
+ });
894
+ const extractedSummary = formatExtractedAssetSummary(extractedAssets);
895
+ if (extractedSummary) console.log(`[build] ${extractedSummary}`);
880
896
  console.log(
881
897
  `[build] 输出: index.js ${fileSizeLabel(entryOutput)} / gzip ${fileGzipSizeLabel(entryOutput)}`,
882
898
  );
@@ -18,6 +18,10 @@ import {
18
18
  } from "./utils/incremental.mjs";
19
19
  import { createNamespaceCssPlugin } from "./utils/namespace-css.mjs";
20
20
  import { validateRuntimeCssFile } from "./utils/runtime-css-check.mjs";
21
+ import {
22
+ extractLargeDataUrlAssets,
23
+ formatExtractedAssetSummary,
24
+ } from "./utils/static-assets.mjs";
21
25
 
22
26
  process.env.NODE_ENV = "production";
23
27
  process.env.BABEL_ENV = "production";
@@ -428,6 +432,12 @@ async function buildSharedRuntime(options = {}) {
428
432
 
429
433
  const runtimeJsPath = path.join(runtimeDistDir, "runtime.js");
430
434
  const runtimeCssPath = path.join(runtimeDistDir, "style.css");
435
+ const extractedAssets = extractLargeDataUrlAssets({
436
+ outDir: runtimeDistDir,
437
+ files: [runtimeJsPath, runtimeCssPath],
438
+ });
439
+ const extractedSummary = formatExtractedAssetSummary(extractedAssets);
440
+ if (extractedSummary) console.log(`[build] ${extractedSummary}`);
431
441
  validateRuntimeCssFile(runtimeCssPath, { label: "代码页共享 runtime" });
432
442
  const runtimeJs = fsSync.readFileSync(runtimeJsPath);
433
443
  const runtimeCss = fsSync.existsSync(runtimeCssPath)
@@ -513,8 +523,10 @@ async function getProxyExportNames(source) {
513
523
  }
514
524
  if (source === "openxiangda") {
515
525
  const parsed = readPackageTypeExports(source);
516
- exportNameCache.set(source, parsed);
517
- return parsed;
526
+ const runtimeExports = await readRuntimeModuleExports(source).catch(() => []);
527
+ const names = Array.from(new Set([...parsed, ...runtimeExports]));
528
+ exportNameCache.set(source, names);
529
+ return names;
518
530
  }
519
531
  if (source === "@ant-design/icons") {
520
532
  const names = await readRuntimeModuleExports(source);
@@ -538,7 +550,8 @@ async function readRuntimeModuleExports(source) {
538
550
 
539
551
  function readPackageTypeExports(source) {
540
552
  try {
541
- const entryPath = require.resolve(source);
553
+ const entryPath = resolvePackageEntry(source);
554
+ if (!entryPath) return [];
542
555
  let currentDir = path.dirname(entryPath);
543
556
  let packageJsonPath = "";
544
557
  while (currentDir && currentDir !== path.dirname(currentDir)) {
@@ -582,6 +595,21 @@ function readPackageTypeExports(source) {
582
595
  }
583
596
  }
584
597
 
598
+ function resolvePackageEntry(source) {
599
+ const attempts = [
600
+ () => require.resolve(source),
601
+ () => require.resolve(source, { paths: [rootDir] }),
602
+ ];
603
+ for (const attempt of attempts) {
604
+ try {
605
+ return attempt();
606
+ } catch {
607
+ // Try the next resolution root.
608
+ }
609
+ }
610
+ return "";
611
+ }
612
+
585
613
  async function createProxyModuleCode(source) {
586
614
  const exportNames = await getProxyExportNames(source);
587
615
  const globalKey = JSON.stringify(source);
@@ -716,6 +744,13 @@ async function buildPage(page) {
716
744
  if (!jsPath) throw new Error(`页面 ${page.config.code} 未生成 index.js`);
717
745
  const cssPath = await ensureFileName(outDir, "style.css", ".css");
718
746
  if (!cssPath) await fs.writeFile(path.join(outDir, "style.css"), "", "utf8");
747
+ const finalCssPath = path.join(outDir, "style.css");
748
+ const extractedAssets = extractLargeDataUrlAssets({
749
+ outDir,
750
+ files: [jsPath, finalCssPath],
751
+ });
752
+ const extractedSummary = formatExtractedAssetSummary(extractedAssets);
753
+ if (extractedSummary) console.log(`[build] ${extractedSummary}`);
719
754
 
720
755
  const outputCode = await fs.readFile(jsPath, "utf8");
721
756
  const bareImportMatch = outputCode.match(
@@ -0,0 +1,165 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const defaultInlineLimit = 4096;
6
+
7
+ const mimeExtensions = new Map([
8
+ ["image/png", ".png"],
9
+ ["image/jpeg", ".jpg"],
10
+ ["image/gif", ".gif"],
11
+ ["image/svg+xml", ".svg"],
12
+ ["image/webp", ".webp"],
13
+ ["image/avif", ".avif"],
14
+ ["image/x-icon", ".ico"],
15
+ ["font/woff", ".woff"],
16
+ ["font/woff2", ".woff2"],
17
+ ["font/ttf", ".ttf"],
18
+ ["application/vnd.ms-fontobject", ".eot"],
19
+ ["application/octet-stream", ".bin"],
20
+ ]);
21
+
22
+ function readInlineLimit() {
23
+ const raw =
24
+ process.env.LOWCODE_WORKSPACE_ASSETS_INLINE_LIMIT ||
25
+ process.env.LOWCODE_ASSETS_INLINE_LIMIT ||
26
+ "";
27
+ if (!raw) return defaultInlineLimit;
28
+ const parsed = Number(raw);
29
+ if (!Number.isFinite(parsed) || parsed < 0) return defaultInlineLimit;
30
+ return parsed;
31
+ }
32
+
33
+ function parseDataUrl(value) {
34
+ if (!value.startsWith("data:")) return null;
35
+ const commaIndex = value.indexOf(",");
36
+ if (commaIndex < 0) return null;
37
+ const meta = value.slice(5, commaIndex);
38
+ const body = value.slice(commaIndex + 1);
39
+ const parts = meta.split(";").filter(Boolean);
40
+ const mime = (parts[0] || "application/octet-stream").toLowerCase();
41
+ if (!parts.slice(1).some((part) => part.toLowerCase() === "base64")) {
42
+ return null;
43
+ }
44
+ if (!/^[A-Za-z0-9+/=]+$/.test(body)) return null;
45
+ return { mime, buffer: Buffer.from(body, "base64") };
46
+ }
47
+
48
+ function resolveAssetFile(parsed, assetCache, assetsDir) {
49
+ const hash = crypto.createHash("sha256").update(parsed.buffer).digest("hex").slice(0, 16);
50
+ const ext = mimeExtensions.get(parsed.mime) || ".bin";
51
+ const cacheKey = `${parsed.mime}:${hash}`;
52
+ const cached = assetCache.get(cacheKey);
53
+ if (cached) return cached;
54
+
55
+ const fileName = `asset-${hash}${ext}`;
56
+ const filePath = path.join(assetsDir, fileName);
57
+ fs.mkdirSync(assetsDir, { recursive: true });
58
+ if (!fs.existsSync(filePath)) {
59
+ fs.writeFileSync(filePath, parsed.buffer);
60
+ }
61
+ const asset = {
62
+ fileName,
63
+ filePath,
64
+ mime: parsed.mime,
65
+ size: parsed.buffer.length,
66
+ };
67
+ assetCache.set(cacheKey, asset);
68
+ return asset;
69
+ }
70
+
71
+ function createExtractor({ inlineLimit, assetsDir, assetCache, emittedAssets }) {
72
+ return function extract(dataUrl) {
73
+ const parsed = parseDataUrl(dataUrl);
74
+ if (!parsed || parsed.buffer.length <= inlineLimit) return null;
75
+ const asset = resolveAssetFile(parsed, assetCache, assetsDir);
76
+ emittedAssets.set(asset.fileName, asset);
77
+ return asset;
78
+ };
79
+ }
80
+
81
+ function rewriteCss(content, extractAsset) {
82
+ let changed = false;
83
+ const code = content.replace(
84
+ /url\(\s*(["']?)(data:[^"'\\)]*?;base64,[A-Za-z0-9+/=]+)\1\s*\)/g,
85
+ (full, _quote, dataUrl) => {
86
+ const asset = extractAsset(dataUrl);
87
+ if (!asset) return full;
88
+ changed = true;
89
+ return `url("assets/${asset.fileName}")`;
90
+ },
91
+ );
92
+ return { changed, code };
93
+ }
94
+
95
+ function rewriteJs(content, extractAsset) {
96
+ let changed = false;
97
+ const code = content.replace(
98
+ /(["'])(data:[^"'\\]*?;base64,[A-Za-z0-9+/=]+)\1/g,
99
+ (full, _quote, dataUrl) => {
100
+ const asset = extractAsset(dataUrl);
101
+ if (!asset) return full;
102
+ changed = true;
103
+ return `new URL("assets/${asset.fileName}", import.meta.url).href`;
104
+ },
105
+ );
106
+ return { changed, code };
107
+ }
108
+
109
+ function rewriteFile(filePath, extractAsset) {
110
+ if (!fs.existsSync(filePath)) return { changed: false };
111
+ const ext = path.extname(filePath).toLowerCase();
112
+ if (ext !== ".css" && ext !== ".js" && ext !== ".mjs") {
113
+ return { changed: false };
114
+ }
115
+ const content = fs.readFileSync(filePath, "utf-8");
116
+ const result = ext === ".css" ? rewriteCss(content, extractAsset) : rewriteJs(content, extractAsset);
117
+ if (result.changed) {
118
+ fs.writeFileSync(filePath, result.code, "utf-8");
119
+ }
120
+ return { changed: result.changed };
121
+ }
122
+
123
+ export function extractLargeDataUrlAssets(options) {
124
+ const outDir = options?.outDir;
125
+ const files = (options?.files || []).filter(Boolean);
126
+ if (!outDir || files.length === 0) {
127
+ return { extracted: [], changedFiles: [] };
128
+ }
129
+
130
+ const inlineLimit =
131
+ typeof options.inlineLimit === "number" ? options.inlineLimit : readInlineLimit();
132
+ const assetsDir = path.join(outDir, options.assetsDir || "assets");
133
+ const assetCache = new Map();
134
+ const emittedAssets = new Map();
135
+ const extractAsset = createExtractor({
136
+ inlineLimit,
137
+ assetsDir,
138
+ assetCache,
139
+ emittedAssets,
140
+ });
141
+ const changedFiles = [];
142
+
143
+ for (const file of files) {
144
+ const result = rewriteFile(file, extractAsset);
145
+ if (result.changed) changedFiles.push(file);
146
+ }
147
+
148
+ return {
149
+ changedFiles,
150
+ extracted: Array.from(emittedAssets.values()),
151
+ inlineLimit,
152
+ };
153
+ }
154
+
155
+ export function formatExtractedAssetSummary(result) {
156
+ const count = result?.extracted?.length || 0;
157
+ if (!count) return "";
158
+ const total = result.extracted.reduce((sum, asset) => sum + asset.size, 0);
159
+ const formatBytes = (bytes) => {
160
+ if (bytes < 1024) return `${bytes} B`;
161
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
162
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
163
+ };
164
+ return `抽离静态资源 ${count} 个,${formatBytes(total)},阈值 ${formatBytes(result.inlineLimit)}`;
165
+ }
@@ -0,0 +1,65 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+
7
+ import { extractLargeDataUrlAssets } from "./static-assets.mjs";
8
+
9
+ let tempDirs: string[] = [];
10
+
11
+ function createTempDir() {
12
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "static-assets-"));
13
+ tempDirs.push(dir);
14
+ return dir;
15
+ }
16
+
17
+ function dataUrl(content: string, mime = "image/png") {
18
+ return `data:${mime};base64,${Buffer.from(content).toString("base64")}`;
19
+ }
20
+
21
+ afterEach(() => {
22
+ for (const dir of tempDirs) {
23
+ fs.rmSync(dir, { recursive: true, force: true });
24
+ }
25
+ tempDirs = [];
26
+ });
27
+
28
+ describe("static asset extraction", () => {
29
+ it("keeps small data URLs inline and extracts large CSS/JS data URLs", () => {
30
+ const outDir = createTempDir();
31
+ const cssPath = path.join(outDir, "style.css");
32
+ const jsPath = path.join(outDir, "index.js");
33
+ const small = dataUrl("tiny");
34
+ const large = dataUrl("large-static-asset-content");
35
+
36
+ fs.writeFileSync(
37
+ cssPath,
38
+ `.hero{background:url("${large}")}.icon{background:url(${small})}`,
39
+ "utf-8",
40
+ );
41
+ fs.writeFileSync(
42
+ jsPath,
43
+ `const hero = "${large}"; const icon = "${small}"; export { hero, icon };`,
44
+ "utf-8",
45
+ );
46
+
47
+ const result = extractLargeDataUrlAssets({
48
+ outDir,
49
+ files: [cssPath, jsPath],
50
+ inlineLimit: 8,
51
+ });
52
+
53
+ expect(result.extracted).toHaveLength(1);
54
+ const [asset] = result.extracted;
55
+ expect(asset.fileName).toMatch(/^asset-[a-f0-9]{16}\.png$/);
56
+ expect(fs.existsSync(path.join(outDir, "assets", asset.fileName))).toBe(true);
57
+
58
+ const css = fs.readFileSync(cssPath, "utf-8");
59
+ const js = fs.readFileSync(jsPath, "utf-8");
60
+ expect(css).toContain(`url("assets/${asset.fileName}")`);
61
+ expect(css).toContain(small);
62
+ expect(js).toContain(`new URL("assets/${asset.fileName}", import.meta.url).href`);
63
+ expect(js).toContain(small);
64
+ });
65
+ });