create-yeow 0.2.102 → 0.2.105
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/templates/default/.yeow/assets/yeow-runtime-0.1.0.jar +0 -0
- package/templates/default/.yeow/build.js +21 -3
- package/templates/default/.yeow/permissions.js +37 -0
- package/templates/default/.yeow/yeow-assets.mjs +192 -87
- package/templates/default/package.json +2 -1
- package/templates/default/tsconfig.json +2 -1
package/package.json
CHANGED
|
Binary file
|
|
@@ -4,7 +4,7 @@ import { resolve, dirname } from 'path';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import AdmZip from 'adm-zip';
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
|
-
import { makeAssetPlugin, makeDedupePlugin, assetsOutDirFor } from './yeow-assets.mjs';
|
|
7
|
+
import { makeAssetPlugin, makeDedupePlugin, assetsOutDirFor, readMergedPermissions } from './yeow-assets.mjs';
|
|
8
8
|
|
|
9
9
|
const root = resolve(fileURLToPath(import.meta.url), '..', '..');
|
|
10
10
|
const cfg = JSON.parse(readFileSync(resolve(root, 'yeow.config.json'), 'utf-8'));
|
|
@@ -37,6 +37,24 @@ async function main() {
|
|
|
37
37
|
// 清空资产输出,避免旧哈希目录残留
|
|
38
38
|
rmSync(assetsOut, { recursive: true, force: true });
|
|
39
39
|
|
|
40
|
+
// ── 计算最终权限(computedPermissions:合并 + 通配归一化)──
|
|
41
|
+
const mergedPerms = readMergedPermissions(root, pkgJson);
|
|
42
|
+
// 回写 computedPermissions 到主项目 yeow.config.json(开发者声明的 permissions 保持原样)
|
|
43
|
+
try {
|
|
44
|
+
const cfgPath = resolve(root, 'yeow.config.json');
|
|
45
|
+
const cfgFile = JSON.parse(readFileSync(cfgPath, 'utf-8'));
|
|
46
|
+
if (JSON.stringify(cfgFile.computedPermissions) !== JSON.stringify(mergedPerms)) {
|
|
47
|
+
cfgFile.computedPermissions = mergedPerms;
|
|
48
|
+
writeFileSync(cfgPath, JSON.stringify(cfgFile, null, 4) + '\n');
|
|
49
|
+
console.log(' \u2713 computedPermissions written back to yeow.config.json');
|
|
50
|
+
}
|
|
51
|
+
} catch (e) { /* 写回失败不阻塞构建 */ }
|
|
52
|
+
if (mergedPerms.length > 0) {
|
|
53
|
+
console.log(' \u2713 Computed permissions (' + mergedPerms.length + '): ' + mergedPerms.join(', '));
|
|
54
|
+
} else {
|
|
55
|
+
console.log(' \u2713 No permissions declared');
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
// ── 打包 ──
|
|
41
59
|
await esbuild.build({
|
|
42
60
|
entryPoints: [resolve(root, entry)],
|
|
@@ -88,7 +106,7 @@ async function main() {
|
|
|
88
106
|
console.log(' \u2713 Assets included (' + files.length + ' files)');
|
|
89
107
|
}
|
|
90
108
|
|
|
91
|
-
zip.addFile('yeow.json', Buffer.from(JSON.stringify(cfg)));
|
|
109
|
+
zip.addFile('yeow.json', Buffer.from(JSON.stringify({ ...cfg, computedPermissions: mergedPerms })));
|
|
92
110
|
const outJar = resolve(root, 'dist', isDev ? 'plugins' : '', name + '-' + version + '.jar');
|
|
93
111
|
mkdirSync(dirname(outJar), { recursive: true });
|
|
94
112
|
zip.writeZip(outJar);
|
|
@@ -110,7 +128,7 @@ async function main() {
|
|
|
110
128
|
pkgZip.addFile('assets/' + f.replace(/\\/g, '/'), readFileSync(resolve(assetsOut, f)));
|
|
111
129
|
}
|
|
112
130
|
}
|
|
113
|
-
pkgZip.addFile('yeow.json', Buffer.from(JSON.stringify(cfg)));
|
|
131
|
+
pkgZip.addFile('yeow.json', Buffer.from(JSON.stringify({ ...cfg, computedPermissions: mergedPerms })));
|
|
114
132
|
const outZip = resolve(root, 'dist', isDev ? 'plugins' : '', name + '-' + version + '.yeow.zip');
|
|
115
133
|
mkdirSync(dirname(outZip), { recursive: true });
|
|
116
134
|
pkgZip.writeZip(outZip);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { collectPermissionsWithSources, readMergedPermissions } from './yeow-assets.mjs';
|
|
5
|
+
|
|
6
|
+
const root = resolve(fileURLToPath(import.meta.url), '..', '..');
|
|
7
|
+
const cfg = JSON.parse(readFileSync(resolve(root, 'yeow.config.json'), 'utf-8'));
|
|
8
|
+
const pkgJson = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf-8'));
|
|
9
|
+
|
|
10
|
+
// ── 权限分布统计(每个权限来自哪个依赖项)──
|
|
11
|
+
const sources = collectPermissionsWithSources(root, pkgJson);
|
|
12
|
+
const computed = readMergedPermissions(root, pkgJson);
|
|
13
|
+
|
|
14
|
+
console.log('── Permissions by source ─────────────────────────');
|
|
15
|
+
let total = 0;
|
|
16
|
+
for (const [perm, owners] of sources) {
|
|
17
|
+
const from = [...owners].join(', ');
|
|
18
|
+
console.log(' ' + perm.padEnd(28) + '← ' + from);
|
|
19
|
+
total += owners.size;
|
|
20
|
+
}
|
|
21
|
+
console.log(' (' + sources.size + ' declarations from ' + new Set([...sources.values()].flatMap(s => [...s])).size + ' packages)');
|
|
22
|
+
|
|
23
|
+
console.log('\n── Computed permissions (' + computed.length + ') ─────────────────');
|
|
24
|
+
for (const p of computed) console.log(' ' + p);
|
|
25
|
+
if (computed.length === 0) console.log(' (none)');
|
|
26
|
+
|
|
27
|
+
// ── 回写 computedPermissions 到 yeow.config.json ──
|
|
28
|
+
const cfgPath = resolve(root, 'yeow.config.json');
|
|
29
|
+
const current = JSON.stringify(cfg.computedPermissions);
|
|
30
|
+
const next = JSON.stringify(computed);
|
|
31
|
+
if (current !== next) {
|
|
32
|
+
cfg.computedPermissions = computed;
|
|
33
|
+
writeFileSync(cfgPath, JSON.stringify(cfg, null, 4) + '\n');
|
|
34
|
+
console.log('\n✓ computedPermissions written back to yeow.config.json');
|
|
35
|
+
} else {
|
|
36
|
+
console.log('\n✓ computedPermissions unchanged');
|
|
37
|
+
}
|
|
@@ -1,113 +1,220 @@
|
|
|
1
|
-
import { readFileSync, readdirSync, existsSync, statSync, mkdirSync, copyFileSync } from 'fs';
|
|
2
|
-
import { resolve
|
|
1
|
+
import { readFileSync, readdirSync, existsSync, statSync, mkdirSync, copyFileSync, realpathSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
|
-
import {
|
|
4
|
+
import { randomBytes } from 'crypto';
|
|
5
5
|
|
|
6
6
|
const require = createRequire(import.meta.url);
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
children[e.name] = buildTree(p);
|
|
19
|
-
} else {
|
|
20
|
-
children[e.name] = {
|
|
21
|
-
isFile: true,
|
|
22
|
-
contentHash: createHash('md5').update(readFileSync(p)).digest('hex').slice(0, 8),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
const parts = Object.keys(children).sort().map(k => {
|
|
27
|
-
const c = children[k];
|
|
28
|
-
return k + ':' + (c.isFile ? c.contentHash : c.hash);
|
|
29
|
-
});
|
|
30
|
-
return { children, isFile: false, hash: createHash('md5').update(parts.join(',')).digest('hex').slice(0, 8) };
|
|
8
|
+
const slash = p => p.replace(/\\/g, '/');
|
|
9
|
+
const normKey = p => slash(resolve(p)).toLowerCase();
|
|
10
|
+
|
|
11
|
+
/** 读取 yeow.config.json 的 permissions(缺失/解析失败 → 空数组)。 */
|
|
12
|
+
function readPerms(configPath) {
|
|
13
|
+
try {
|
|
14
|
+
const j = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
15
|
+
if (Array.isArray(j.permissions)) return j.permissions.filter(x => typeof x === 'string');
|
|
16
|
+
} catch { /* 无 yeow.config.json 或解析失败 */ }
|
|
17
|
+
return [];
|
|
31
18
|
}
|
|
32
19
|
|
|
33
|
-
// ──
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
20
|
+
// ── 依赖项收集(node_modules 扫描)─────────────────────────────
|
|
21
|
+
// 规则:
|
|
22
|
+
// - 主项目无条件参与(始终分配 id,保证 getAssetsPath 恒可用;有 assets/ 才复制)
|
|
23
|
+
// - 依赖包:node_modules 顶层目录(含 @scope/name 两级),要求
|
|
24
|
+
// assets/ 目录存在 且 peerDependencies 含 yeow-api 键
|
|
25
|
+
// - 每个候选同时读取其 yeow.config.json 的 permissions(依赖包可自行声明权限)
|
|
26
|
+
// 键:<name>-<version>。npm/pnpm 扁平布局支持良好;yarn 的 hoisting
|
|
27
|
+
// 差异可能导致依赖不在预期位置(见文档说明)。
|
|
28
|
+
function collectCandidates(root, pkgJson) {
|
|
29
|
+
const candidates = [];
|
|
30
|
+
const ownAssets = resolve(root, 'assets');
|
|
31
|
+
candidates.push({
|
|
32
|
+
key: pkgJson.name + '-' + pkgJson.version,
|
|
33
|
+
pkgDir: root,
|
|
34
|
+
absSrc: ownAssets,
|
|
35
|
+
hasAssets: existsSync(ownAssets),
|
|
36
|
+
perms: readPerms(resolve(root, 'yeow.config.json')),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const nm = resolve(root, 'node_modules');
|
|
40
|
+
if (existsSync(nm)) {
|
|
41
|
+
const names = [];
|
|
42
|
+
for (const entry of readdirSync(nm)) {
|
|
43
|
+
const p = resolve(nm, entry);
|
|
44
|
+
let st;
|
|
45
|
+
try { st = statSync(p); } catch { continue; } // statSync 跟随 symlink(pnpm)
|
|
46
|
+
if (!st.isDirectory()) continue;
|
|
47
|
+
if (entry.startsWith('@')) {
|
|
48
|
+
for (const sub of readdirSync(p)) {
|
|
49
|
+
const sp = resolve(p, sub);
|
|
50
|
+
try { if (statSync(sp).isDirectory()) names.push(entry + '/' + sub); } catch { /* 跳过 */ }
|
|
52
51
|
}
|
|
53
|
-
|
|
52
|
+
} else {
|
|
53
|
+
names.push(entry);
|
|
54
54
|
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
55
|
+
}
|
|
56
|
+
for (const name of names) {
|
|
57
|
+
const pkgDir = resolve(nm, ...name.split('/'));
|
|
58
|
+
let meta;
|
|
59
|
+
try { meta = JSON.parse(readFileSync(resolve(pkgDir, 'package.json'), 'utf-8')); } catch { continue; }
|
|
60
|
+
if (!meta.peerDependencies || !meta.peerDependencies['yeow-api']) continue;
|
|
61
|
+
const pkgAssets = resolve(pkgDir, 'assets');
|
|
62
|
+
if (!existsSync(pkgAssets)) continue;
|
|
63
|
+
candidates.push({
|
|
64
|
+
key: meta.name + '-' + (meta.version || '0.0.0'),
|
|
65
|
+
pkgDir,
|
|
66
|
+
absSrc: pkgAssets,
|
|
67
|
+
hasAssets: true,
|
|
68
|
+
perms: readPerms(resolve(pkgDir, 'yeow.config.json')),
|
|
69
|
+
});
|
|
63
70
|
}
|
|
64
71
|
}
|
|
72
|
+
return candidates;
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
/**
|
|
76
|
+
* 收集每个权限节点的来源:permission → 声明它的依赖项键集合(主项目 key 在前)。
|
|
77
|
+
* 仅直接依赖(node_modules 顶层)参与——依赖包的依赖所需权限无需计算。
|
|
78
|
+
*/
|
|
79
|
+
export function collectPermissionsWithSources(root, pkgJson) {
|
|
80
|
+
const map = new Map(); // perm → Set<key>
|
|
81
|
+
for (const c of collectCandidates(root, pkgJson)) {
|
|
82
|
+
for (const p of c.perms) {
|
|
83
|
+
if (!map.has(p)) map.set(p, new Set());
|
|
84
|
+
map.get(p).add(c.key);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return map;
|
|
88
|
+
}
|
|
72
89
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
/**
|
|
91
|
+
* 计算最终生效权限(computedPermissions):合并主项目与全部依赖包的
|
|
92
|
+
* 声明(主项目在前、依赖包按收集顺序追加、去重),再做通配归一化:
|
|
93
|
+
* - `X:*`(如 fs:*)覆盖全部 `X:...` 节点(含二级通配与子节点)
|
|
94
|
+
* - `X:level.*`(如 fs:server.*)覆盖 `X:level.<op>` 子节点
|
|
95
|
+
* 最后把 `fs:*` 展开为 `fs:outer.*, fs:server.*`——权限语义等价
|
|
96
|
+
* (plugin 级免声明),但让开发者/服主对实际影响范围有明确感知。
|
|
97
|
+
*/
|
|
98
|
+
export function readMergedPermissions(root, pkgJson) {
|
|
99
|
+
const merged = [];
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (const c of collectCandidates(root, pkgJson)) {
|
|
102
|
+
for (const p of c.perms) {
|
|
103
|
+
if (!seen.has(p)) { seen.add(p); merged.push(p); }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const channelWildcards = new Set(); // X:*
|
|
107
|
+
const levelWildcards = new Set(); // X:level.*
|
|
108
|
+
for (const p of merged) {
|
|
109
|
+
if (p.endsWith(':*')) channelWildcards.add(p.slice(0, -2));
|
|
110
|
+
else if (p.endsWith('.*')) levelWildcards.add(p.slice(0, -2));
|
|
111
|
+
}
|
|
112
|
+
let normalized = merged;
|
|
113
|
+
if (channelWildcards.size > 0 || levelWildcards.size > 0) {
|
|
114
|
+
normalized = merged.filter(p => {
|
|
115
|
+
if (p.endsWith(':*')) return true;
|
|
116
|
+
const dot = p.lastIndexOf('.');
|
|
117
|
+
const levelPrefix = dot > 0 ? p.slice(0, dot) : null; // fs:server.readFile → fs:server
|
|
118
|
+
const col = p.indexOf(':');
|
|
119
|
+
const channel = col > 0 ? p.slice(0, col) : null; // fs:server.readFile → fs
|
|
120
|
+
if (p.endsWith('.*')) return channel === null || !channelWildcards.has(channel);
|
|
121
|
+
if (channel !== null && channelWildcards.has(channel)) return false;
|
|
122
|
+
if (levelPrefix !== null && levelWildcards.has(levelPrefix)) return false;
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
81
125
|
}
|
|
82
|
-
|
|
126
|
+
// fs:* 展开(语义等价:fs:outer.* + fs:server.* 覆盖各自级别,plugin 级免声明)
|
|
127
|
+
if (normalized.includes('fs:*')) {
|
|
128
|
+
const out = [];
|
|
129
|
+
for (const p of normalized) {
|
|
130
|
+
if (p === 'fs:*') { out.push('fs:outer.*', 'fs:server.*'); }
|
|
131
|
+
else out.push(p);
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
return normalized;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── id 分配(8 位 hex,不哈希内容,仅保证构建内唯一)──────────
|
|
139
|
+
// 每个依赖项的资产部署到 .assets/<id>/,id 唯一 → 无同名冲突。
|
|
140
|
+
function assignIds(candidates) {
|
|
141
|
+
const used = new Set();
|
|
142
|
+
const ids = new Map(); // 路径(realpath + 原始路径,lowercase)→ id
|
|
143
|
+
for (const c of candidates) {
|
|
144
|
+
let id;
|
|
145
|
+
do { id = randomBytes(4).toString('hex'); } while (used.has(id));
|
|
146
|
+
used.add(id);
|
|
147
|
+
c.id = id;
|
|
148
|
+
ids.set(normKey(c.pkgDir), id);
|
|
149
|
+
try { ids.set(normKey(realpathSync(c.pkgDir)), id); } catch { /* 保持原始路径 */ }
|
|
150
|
+
}
|
|
151
|
+
return ids;
|
|
83
152
|
}
|
|
84
153
|
|
|
85
|
-
// ──
|
|
154
|
+
// ── 原样部署到 .assets/<id>/(无改名,相对引用天然有效)────────
|
|
155
|
+
function copyDir(src, dst) {
|
|
156
|
+
mkdirSync(dst, { recursive: true });
|
|
157
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
158
|
+
const s = resolve(src, entry.name);
|
|
159
|
+
const d = resolve(dst, entry.name);
|
|
160
|
+
if (entry.isDirectory()) copyDir(s, d);
|
|
161
|
+
else copyFileSync(s, d);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function deployAll(candidates, assetsOutDir) {
|
|
166
|
+
for (const c of candidates) {
|
|
167
|
+
if (!c.hasAssets) continue;
|
|
168
|
+
copyDir(c.absSrc, resolve(assetsOutDir, c.id));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── esbuild 插件:yeow-dev 虚拟模块 ────────────────────────────
|
|
173
|
+
// yeow-dev 是构建期模块(发布为空包):getAssetsPath 由构建器按
|
|
174
|
+
// importer 所属依赖项注入对应命名空间 id,运行时无需任何改动。
|
|
86
175
|
export function makeAssetPlugin({ root, pkgJson, outDir }) {
|
|
87
176
|
const assetsOutDir = resolve(outDir, '.assets');
|
|
88
177
|
return {
|
|
89
178
|
name: 'yeow-assets',
|
|
90
179
|
setup(build) {
|
|
91
|
-
|
|
92
|
-
|
|
180
|
+
const candidates = collectCandidates(root, pkgJson);
|
|
181
|
+
const ids = assignIds(candidates);
|
|
182
|
+
const rootId = ids.get(normKey(root));
|
|
183
|
+
deployAll(candidates, assetsOutDir);
|
|
184
|
+
|
|
185
|
+
// importer → 所属依赖项 id(最长路径前缀匹配;未匹配归主项目)
|
|
186
|
+
const idForImporter = importer => {
|
|
187
|
+
const imp = normKey(importer);
|
|
188
|
+
let best = null;
|
|
189
|
+
let bestLen = -1;
|
|
190
|
+
for (const [dir, id] of ids) {
|
|
191
|
+
if (dir === imp || imp.startsWith(dir + '/')) {
|
|
192
|
+
if (dir.length > bestLen) { best = id; bestLen = dir.length; }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return best || rootId;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
build.onResolve({ filter: /^yeow-dev$/ }, args => ({
|
|
199
|
+
path: 'yeow-dev?id=' + idForImporter(args.importer),
|
|
93
200
|
namespace: 'yeow-assets',
|
|
94
201
|
}));
|
|
95
|
-
build.onLoad({ filter: /.*/, namespace: 'yeow-assets' },
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
for (const src of collectSources(root, pkgJson)) {
|
|
99
|
-
deploy(src.tree, src.absSrc, assetsOutDir, map, '', '', src.overwrite, true);
|
|
100
|
-
}
|
|
202
|
+
build.onLoad({ filter: /.*/, namespace: 'yeow-assets' }, args => {
|
|
203
|
+
const m = /yeow-dev\?id=([0-9a-f]+)/.exec(args.path);
|
|
204
|
+
const id = m ? m[1] : rootId;
|
|
101
205
|
return {
|
|
102
206
|
contents:
|
|
103
|
-
'const
|
|
104
|
-
'function
|
|
105
|
-
'
|
|
106
|
-
'
|
|
107
|
-
' const
|
|
108
|
-
'
|
|
109
|
-
'
|
|
110
|
-
'
|
|
207
|
+
'const _id = ' + JSON.stringify(id) + ';\n' +
|
|
208
|
+
'export function getAssetsPath(p) {\n' +
|
|
209
|
+
' const parts = String(p).replace(/\\\\/g, "/").split("/");\n' +
|
|
210
|
+
' const out = [];\n' +
|
|
211
|
+
' for (const s of parts) {\n' +
|
|
212
|
+
' if (!s || s === ".") continue;\n' +
|
|
213
|
+
' if (s === "..") { if (out.length) out.pop(); continue; }\n' +
|
|
214
|
+
' out.push(s);\n' +
|
|
215
|
+
' }\n' +
|
|
216
|
+
' const trailing = /[\\/\\\\]$/.test(String(p)) ? "/" : "";\n' +
|
|
217
|
+
' return "assets/" + _id + (out.length ? "/" + out.join("/") : "") + trailing;\n' +
|
|
111
218
|
'}\n',
|
|
112
219
|
loader: 'js',
|
|
113
220
|
};
|
|
@@ -137,5 +244,3 @@ export function makeDedupePlugin(root) {
|
|
|
137
244
|
export function assetsOutDirFor(outDir) {
|
|
138
245
|
return resolve(outDir, '.assets');
|
|
139
246
|
}
|
|
140
|
-
|
|
141
|
-
export { buildTree };
|