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 +1 -1
- package/packages/sdk/src/build-source/scripts/build-forms.mjs +16 -0
- package/packages/sdk/src/build-source/scripts/build-pages.mjs +38 -3
- package/packages/sdk/src/build-source/scripts/utils/static-assets.mjs +165 -0
- package/packages/sdk/src/build-source/scripts/utils/static-assets.test.ts +65 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
517
|
-
|
|
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 =
|
|
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
|
+
});
|