@expo-harmony/prebuild-config 55.0.0-harmony.1
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/README.md +68 -0
- package/build/index.js +26 -0
- package/package.json +87 -0
- package/src/api/signing.ts +4 -0
- package/src/buildDescriptor.ts +277 -0
- package/src/check.ts +390 -0
- package/src/dependencies.ts +123 -0
- package/src/errors.ts +23 -0
- package/src/generatedFiles.ts +39 -0
- package/src/index.ts +25 -0
- package/src/manifest.ts +303 -0
- package/src/mods/withAutolinkingMods.ts +92 -0
- package/src/mods/withEntryMods.ts +332 -0
- package/src/mods/withPreparationMod.ts +85 -0
- package/src/mods/withProjectMods.ts +258 -0
- package/src/mods/withSourceMods.ts +32 -0
- package/src/native.ts +10 -0
- package/src/packageMetadata.ts +8 -0
- package/src/plugin.ts +7 -0
- package/src/reconcile.ts +70 -0
- package/src/renderers.ts +90 -0
- package/src/signing.ts +251 -0
- package/src/stale.ts +103 -0
- package/src/template.ts +172 -0
- package/src/withHarmonyPrebuildConfig.ts +47 -0
- package/tsconfig.json +20 -0
package/src/native.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
function isRnohAutolinkingDisabled(source: string): boolean {
|
|
2
|
+
if (typeof source !== 'string' || !source.trim()) {
|
|
3
|
+
throw new TypeError('module hvigorfile.ts must start from the canonical Harmony template source.');
|
|
4
|
+
}
|
|
5
|
+
return /\bautolinking\s*:\s*null\b/u.test(source.replace(/\r\n?/gu, '\n'));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
isRnohAutolinkingDisabled,
|
|
10
|
+
};
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createRunOncePlugin } from '@expo/config-plugins';
|
|
2
|
+
|
|
3
|
+
import { PackageMetadata } from './packageMetadata';
|
|
4
|
+
import { withHarmonyPrebuildConfig } from './withHarmonyPrebuildConfig';
|
|
5
|
+
|
|
6
|
+
export const HarmonyPrebuildPlugin
|
|
7
|
+
= createRunOncePlugin(withHarmonyPrebuildConfig, PackageMetadata.name, PackageMetadata.version);
|
package/src/reconcile.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { stableHarmonyJson } from '@expo-harmony/config-plugins';
|
|
2
|
+
|
|
3
|
+
import { HarmonyPrebuildError } from './errors';
|
|
4
|
+
|
|
5
|
+
function readRecord(value: unknown): Record<string, unknown> {
|
|
6
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function appendUnique(items, values) {
|
|
10
|
+
return [...new Map([...(items || []), ...values].map(value => [stableHarmonyJson(value), value])).values()];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function upsertNamed(items, name, create) {
|
|
14
|
+
const values = Array.isArray(items) ? items : [];
|
|
15
|
+
const existing = values.find(item => item?.name === name);
|
|
16
|
+
const remaining = values.filter(item => item?.name !== name);
|
|
17
|
+
|
|
18
|
+
remaining.push(create(existing && typeof existing === 'object' ? existing : {}));
|
|
19
|
+
|
|
20
|
+
return remaining;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function upsertManagedNamed(items, name, previousName, placeholderName, create, field) {
|
|
24
|
+
const values = Array.isArray(items) ? items : [];
|
|
25
|
+
const previous = typeof previousName === 'string' && previousName ? previousName : placeholderName;
|
|
26
|
+
const named = values.filter(item => item?.name === name);
|
|
27
|
+
const stale = values.filter(item => item?.name === previous);
|
|
28
|
+
|
|
29
|
+
if (named.length > 1 || stale.length > 1) {
|
|
30
|
+
throw new HarmonyPrebuildError(
|
|
31
|
+
'ERR_HARMONY_IDENTITY_COLLISION',
|
|
32
|
+
`Harmony ${field} identity is ambiguous because its profile contains duplicate named entries.`,
|
|
33
|
+
{ operation: 'reconcile-identity' }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (name !== previous && named.length > 0) {
|
|
37
|
+
throw new HarmonyPrebuildError(
|
|
38
|
+
'ERR_HARMONY_IDENTITY_COLLISION',
|
|
39
|
+
`Cannot rename the managed Harmony ${field} from '${previous}' to '${name}' because '${name}' is already user-owned.`,
|
|
40
|
+
{ operation: 'reconcile-identity' }
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const names = new Set([name, previous]);
|
|
45
|
+
const index = values.findIndex(item => names.has(item?.name));
|
|
46
|
+
const existing = index === -1 ? undefined : values[index];
|
|
47
|
+
const next = create(existing && typeof existing === 'object' ? existing : {});
|
|
48
|
+
|
|
49
|
+
if (index === -1) return [...values, next];
|
|
50
|
+
|
|
51
|
+
return values.map((item, current) => current === index ? next : item);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function replaceManagedString(items, value, previousValue, placeholderValue) {
|
|
55
|
+
const previous = typeof previousValue === 'string' && previousValue ? previousValue : placeholderValue;
|
|
56
|
+
|
|
57
|
+
return [...new Set(
|
|
58
|
+
(Array.isArray(items) ? items : [])
|
|
59
|
+
.filter(item => item !== value && item !== previous)
|
|
60
|
+
.concat(value)
|
|
61
|
+
)];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
appendUnique,
|
|
66
|
+
readRecord,
|
|
67
|
+
replaceManagedString,
|
|
68
|
+
upsertManagedNamed,
|
|
69
|
+
upsertNamed,
|
|
70
|
+
};
|
package/src/renderers.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { isRnohAutolinkingDisabled } from './native';
|
|
2
|
+
import type { HarmonyBuildDescriptor } from './buildDescriptor';
|
|
3
|
+
import { TEMPLATE_PLACEHOLDERS } from './template';
|
|
4
|
+
|
|
5
|
+
function normalizeSource(source, label) {
|
|
6
|
+
if (typeof source !== 'string' || !source.trim()) {
|
|
7
|
+
throw new TypeError(`${label} must start from the canonical Harmony template source.`);
|
|
8
|
+
}
|
|
9
|
+
return source.replace(/\r\n?/gu, '\n');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function replacePlaceholder(source, placeholder, value, label) {
|
|
13
|
+
const count = source.split(placeholder).length - 1;
|
|
14
|
+
|
|
15
|
+
if (count !== 1) {
|
|
16
|
+
throw new TypeError(
|
|
17
|
+
`Canonical Harmony template has ${count} ${label} placeholders; expected exactly one.`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return source.replace(placeholder, () => value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function quoteSingle(value) {
|
|
25
|
+
return `'${String(value)
|
|
26
|
+
.replace(/\\/gu, '\\\\')
|
|
27
|
+
.replace(/'/gu, `\\'`)
|
|
28
|
+
.replace(/\r/gu, '\\r')
|
|
29
|
+
.replace(/\n/gu, '\\n')}'`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderReactNativeConfig() {
|
|
33
|
+
return `// Generated by @expo-harmony/prebuild-config. Customize through withReactNativeConfig.
|
|
34
|
+
// React Native CLI loads this generated JavaScript file through CommonJS.
|
|
35
|
+
module.exports = require('@react-native-oh/react-native-harmony-cli/react-native.config.js');
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function renderRootHvigor(source, build: HarmonyBuildDescriptor) {
|
|
40
|
+
source = normalizeSource(source, build.projectFiles.rootHvigor);
|
|
41
|
+
|
|
42
|
+
return replacePlaceholder(
|
|
43
|
+
source,
|
|
44
|
+
quoteSingle(TEMPLATE_PLACEHOLDERS.bundlePath),
|
|
45
|
+
quoteSingle(build.export.bundle),
|
|
46
|
+
'embedded bundle path'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderEntryAbility(source, harmony) {
|
|
51
|
+
source = normalizeSource(source, 'EntryAbility.ets');
|
|
52
|
+
|
|
53
|
+
return replacePlaceholder(
|
|
54
|
+
source,
|
|
55
|
+
TEMPLATE_PLACEHOLDERS.abilityName,
|
|
56
|
+
harmony.abilityName,
|
|
57
|
+
'ability class'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderIndexPage(source, harmony) {
|
|
62
|
+
source = normalizeSource(source, 'Index.ets');
|
|
63
|
+
|
|
64
|
+
return replacePlaceholder(
|
|
65
|
+
source,
|
|
66
|
+
JSON.stringify(TEMPLATE_PLACEHOLDERS.appLabel),
|
|
67
|
+
JSON.stringify(harmony.label),
|
|
68
|
+
'RN instance name'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderCanonical(source, label) {
|
|
73
|
+
return normalizeSource(source, label);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const renderArktsPackageProvider = source => renderCanonical(source, 'PackageProvider.ets');
|
|
77
|
+
const renderCmakeLists = source => renderCanonical(source, 'CMakeLists.txt');
|
|
78
|
+
const renderCppPackageProvider = source => renderCanonical(source, 'PackageProvider.cpp');
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
isRnohAutolinkingDisabled,
|
|
82
|
+
renderArktsPackageProvider,
|
|
83
|
+
renderCanonical,
|
|
84
|
+
renderCmakeLists,
|
|
85
|
+
renderCppPackageProvider,
|
|
86
|
+
renderEntryAbility,
|
|
87
|
+
renderIndexPage,
|
|
88
|
+
renderReactNativeConfig,
|
|
89
|
+
renderRootHvigor,
|
|
90
|
+
};
|
package/src/signing.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import JSON5 from 'json5';
|
|
5
|
+
|
|
6
|
+
import { HarmonyPlatformDirectory } from './buildDescriptor';
|
|
7
|
+
import { HarmonyPrebuildError } from './errors';
|
|
8
|
+
|
|
9
|
+
const MaterialFields = [
|
|
10
|
+
'certpath', 'storePassword', 'keyAlias', 'keyPassword',
|
|
11
|
+
'profile', 'signAlg', 'storeFile',
|
|
12
|
+
] as const;
|
|
13
|
+
type MaterialField = typeof MaterialFields[number];
|
|
14
|
+
type MaterialPathField = Extract<MaterialField, 'certpath' | 'profile' | 'storeFile'>;
|
|
15
|
+
const MaterialPathFields = new Set<MaterialField>(['certpath', 'profile', 'storeFile']);
|
|
16
|
+
|
|
17
|
+
interface SigningFile {
|
|
18
|
+
config: {
|
|
19
|
+
material: Record<MaterialField, string>;
|
|
20
|
+
name: string;
|
|
21
|
+
type: 'HarmonyOS';
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
};
|
|
24
|
+
file: string;
|
|
25
|
+
materialFiles: Partial<Record<MaterialPathField, string>>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface HarmonySigningConfig {
|
|
29
|
+
file: string;
|
|
30
|
+
materialFiles: Partial<Record<'certpath' | 'profile' | 'storeFile', string>>;
|
|
31
|
+
name: string;
|
|
32
|
+
type: 'HarmonyOS';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isInside(root, target) {
|
|
36
|
+
const relative = path.relative(root, target);
|
|
37
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function mirrorAbsolutePath(temp, source) {
|
|
41
|
+
const absolute = path.resolve(source);
|
|
42
|
+
const parsed = path.parse(absolute);
|
|
43
|
+
const volume = parsed.root.replace(/[^A-Za-z0-9]+/gu, '') || 'root';
|
|
44
|
+
const segments = absolute.slice(parsed.root.length).split(path.sep).filter(Boolean);
|
|
45
|
+
|
|
46
|
+
return path.join(temp, 'filesystem', volume, ...segments);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveSigningPath(base, reference) {
|
|
50
|
+
const resolved = path.resolve(base, reference);
|
|
51
|
+
const mirror = process.env.EXPO_HARMONY_CHECK_MIRROR_ROOT;
|
|
52
|
+
|
|
53
|
+
return mirror && path.isAbsolute(reference)
|
|
54
|
+
? mirrorAbsolutePath(path.resolve(mirror), resolved)
|
|
55
|
+
: resolved;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function selectSigningConfig(parsed, file) {
|
|
59
|
+
const source = parsed?.app?.signingConfigs ?? parsed?.signingConfigs ?? parsed;
|
|
60
|
+
const candidates = Array.isArray(source) ? source : [source];
|
|
61
|
+
const configs = candidates.filter(item => item && typeof item === 'object' && !Array.isArray(item));
|
|
62
|
+
|
|
63
|
+
if (configs.length !== candidates.length || configs.length === 0) {
|
|
64
|
+
throw new HarmonyPrebuildError(
|
|
65
|
+
'ERR_HARMONY_SIGNING_INVALID',
|
|
66
|
+
'Signing config file must contain a signing config object or a non-empty signingConfigs array.',
|
|
67
|
+
{ file, operation: 'validate-signing' }
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const config = configs.find(item => item.name === 'default') || (configs.length === 1 ? configs[0] : null);
|
|
72
|
+
|
|
73
|
+
if (!config) {
|
|
74
|
+
throw new HarmonyPrebuildError(
|
|
75
|
+
'ERR_HARMONY_SIGNING_INVALID',
|
|
76
|
+
'Signing config file contains multiple entries but none is named default.',
|
|
77
|
+
{ file, operation: 'validate-signing' }
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return config;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function readSigningConfigFile(root: string, reference: string): Promise<SigningFile> {
|
|
85
|
+
const file = resolveSigningPath(root, reference);
|
|
86
|
+
const harmony = path.join(root, HarmonyPlatformDirectory);
|
|
87
|
+
|
|
88
|
+
if (isInside(harmony, file)) {
|
|
89
|
+
throw new HarmonyPrebuildError(
|
|
90
|
+
'ERR_HARMONY_SIGNING_INVALID',
|
|
91
|
+
'harmony.signingConfigFile must be outside the generated harmony directory so --clean cannot delete it.',
|
|
92
|
+
{ file, operation: 'validate-signing' }
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let content;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const stat = await fs.stat(file);
|
|
100
|
+
if (!stat.isFile()) {
|
|
101
|
+
throw new HarmonyPrebuildError(
|
|
102
|
+
'ERR_HARMONY_SIGNING_INVALID',
|
|
103
|
+
'Harmony signing config reference must be a regular file.',
|
|
104
|
+
{ file, operation: 'validate-signing' }
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
content = await fs.readFile(file, 'utf8');
|
|
109
|
+
} catch (cause) {
|
|
110
|
+
if (cause?.code === 'ERR_HARMONY_SIGNING_INVALID') return Promise.reject(cause);
|
|
111
|
+
|
|
112
|
+
throw new HarmonyPrebuildError(
|
|
113
|
+
'ERR_HARMONY_SIGNING_INVALID',
|
|
114
|
+
'Cannot read harmony.signingConfigFile.',
|
|
115
|
+
{ cause, file, operation: 'validate-signing' }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let parsed;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
parsed = JSON5.parse(content);
|
|
123
|
+
} catch (cause) {
|
|
124
|
+
throw new HarmonyPrebuildError(
|
|
125
|
+
'ERR_HARMONY_SIGNING_INVALID',
|
|
126
|
+
'Cannot parse harmony.signingConfigFile as JSON5.',
|
|
127
|
+
{ cause, file, operation: 'validate-signing' }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const config = selectSigningConfig(parsed, file);
|
|
132
|
+
|
|
133
|
+
if (typeof config.name !== 'string' || !config.name.trim()) {
|
|
134
|
+
throw new HarmonyPrebuildError(
|
|
135
|
+
'ERR_HARMONY_SIGNING_INVALID',
|
|
136
|
+
'Harmony signing config name must be a non-empty string.',
|
|
137
|
+
{ file, operation: 'validate-signing' }
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (config.type !== undefined && config.type !== 'HarmonyOS') {
|
|
141
|
+
throw new HarmonyPrebuildError(
|
|
142
|
+
'ERR_HARMONY_SIGNING_INVALID',
|
|
143
|
+
'Harmony signing config type must be HarmonyOS.',
|
|
144
|
+
{ file, operation: 'validate-signing' }
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (!config.material || typeof config.material !== 'object' || Array.isArray(config.material)) {
|
|
148
|
+
throw new HarmonyPrebuildError(
|
|
149
|
+
'ERR_HARMONY_SIGNING_INVALID',
|
|
150
|
+
'Harmony signing config material must be an object.',
|
|
151
|
+
{ file, operation: 'validate-signing' }
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const material = { ...config.material };
|
|
156
|
+
const files: Partial<Record<MaterialPathField, string>> = {};
|
|
157
|
+
|
|
158
|
+
for (const field of MaterialFields) {
|
|
159
|
+
const value = material[field];
|
|
160
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
161
|
+
throw new HarmonyPrebuildError(
|
|
162
|
+
'ERR_HARMONY_SIGNING_INVALID',
|
|
163
|
+
`Harmony signing material field ${field} must be a non-empty string.`,
|
|
164
|
+
{ file, operation: 'validate-signing' }
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (MaterialPathFields.has(field)) {
|
|
169
|
+
const resolved = resolveSigningPath(path.dirname(file), value);
|
|
170
|
+
|
|
171
|
+
if (isInside(harmony, resolved)) {
|
|
172
|
+
throw new HarmonyPrebuildError(
|
|
173
|
+
'ERR_HARMONY_SIGNING_INVALID',
|
|
174
|
+
`Harmony signing material field ${field} must be outside the generated harmony directory so --clean cannot delete it.`,
|
|
175
|
+
{ file, operation: 'validate-signing' }
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const stat = await fs.stat(resolved);
|
|
181
|
+
if (!stat.isFile()) {
|
|
182
|
+
throw new HarmonyPrebuildError(
|
|
183
|
+
'ERR_HARMONY_SIGNING_INVALID',
|
|
184
|
+
`Harmony signing material field ${field} must reference a regular file.`,
|
|
185
|
+
{ file, operation: 'validate-signing' }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
} catch (cause) {
|
|
189
|
+
if (cause?.code === 'ERR_HARMONY_SIGNING_INVALID') return Promise.reject(cause);
|
|
190
|
+
|
|
191
|
+
throw new HarmonyPrebuildError(
|
|
192
|
+
'ERR_HARMONY_SIGNING_INVALID',
|
|
193
|
+
`Cannot read the file referenced by Harmony signing material field ${field}.`,
|
|
194
|
+
{ cause, file, operation: 'validate-signing' }
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const relative = path.relative(harmony, resolved);
|
|
199
|
+
|
|
200
|
+
if (!relative || path.isAbsolute(relative)) {
|
|
201
|
+
throw new HarmonyPrebuildError(
|
|
202
|
+
'ERR_HARMONY_SIGNING_INVALID',
|
|
203
|
+
`Harmony signing material field ${field} must be on the same filesystem volume as the project.`,
|
|
204
|
+
{ file, operation: 'validate-signing' }
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
material[field] = relative.split(path.sep).join('/');
|
|
209
|
+
files[field as MaterialPathField] = resolved;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (material.signAlg !== 'SHA256withECDSA') {
|
|
214
|
+
throw new HarmonyPrebuildError(
|
|
215
|
+
'ERR_HARMONY_SIGNING_INVALID',
|
|
216
|
+
'Harmony signing material signAlg must be SHA256withECDSA.',
|
|
217
|
+
{ file, operation: 'validate-signing' }
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
config: {
|
|
223
|
+
...config,
|
|
224
|
+
name: config.name.trim(),
|
|
225
|
+
type: 'HarmonyOS',
|
|
226
|
+
material,
|
|
227
|
+
},
|
|
228
|
+
file,
|
|
229
|
+
materialFiles: files,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function validateHarmonySigningConfigFile(root: string, reference: string): Promise<HarmonySigningConfig> {
|
|
234
|
+
const signing = await readSigningConfigFile(root, reference);
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
file: signing.file,
|
|
238
|
+
materialFiles: signing.materialFiles,
|
|
239
|
+
name: signing.config.name,
|
|
240
|
+
type: signing.config.type,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export {
|
|
245
|
+
MaterialFields,
|
|
246
|
+
readSigningConfigFile,
|
|
247
|
+
validateHarmonySigningConfigFile,
|
|
248
|
+
};
|
|
249
|
+
export type {
|
|
250
|
+
HarmonySigningConfig,
|
|
251
|
+
};
|
package/src/stale.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { resolveHarmonyBuildPath } from './buildDescriptor';
|
|
5
|
+
import { HarmonyPrebuildError } from './errors';
|
|
6
|
+
import { CngManifestPath, validateCngManifest } from './manifest';
|
|
7
|
+
|
|
8
|
+
async function readPreviousCngManifestAsync(root) {
|
|
9
|
+
const file = resolveHarmonyBuildPath(root, CngManifestPath);
|
|
10
|
+
|
|
11
|
+
let parsed;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(await fs.promises.readFile(file, 'utf8'));
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error.code === 'ENOENT' || error instanceof SyntaxError) return null;
|
|
17
|
+
return Promise.reject(error);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return validateCngManifest(parsed, { file });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function findStaleConfigPlugins(manifest, plugins) {
|
|
24
|
+
const owners = new Set(plugins.map(plugin => plugin.owner));
|
|
25
|
+
|
|
26
|
+
return (manifest?.configPlugins || []).filter(plugin => !owners.has(plugin.owner));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function removeStalePluginFilesAsync(root, manifest, plugins) {
|
|
30
|
+
const owners = new Set(plugins.map(plugin => plugin.owner));
|
|
31
|
+
const files = (manifest?.managedFiles || []).filter(descriptor => owners.has(descriptor.owner));
|
|
32
|
+
|
|
33
|
+
for (const descriptor of files) {
|
|
34
|
+
const target = path.join(root, ...descriptor.path.split('/'));
|
|
35
|
+
|
|
36
|
+
let stat;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
stat = await fs.promises.lstat(target);
|
|
40
|
+
} catch (cause) {
|
|
41
|
+
if (cause.code === 'ENOENT') continue;
|
|
42
|
+
return Promise.reject(cause);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (stat.isDirectory()) {
|
|
46
|
+
throw new HarmonyPrebuildError(
|
|
47
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
48
|
+
`A stale config plugin claims a managed directory instead of a file: ${descriptor.path}`,
|
|
49
|
+
{ file: target, operation: 'remove-stale-plugin-output' }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await fs.promises.rm(target, { force: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Keep missing files in the result so a retry also removes their declarations.
|
|
57
|
+
return files.map(descriptor => descriptor.path);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function removeStaleExtensionAbilities(module, moduleJsonPath: string, files: readonly string[]) {
|
|
61
|
+
if (!Array.isArray(module.extensionAbilities) || files.length === 0) return module;
|
|
62
|
+
|
|
63
|
+
const stale = new Set(files);
|
|
64
|
+
const directory = path.posix.dirname(moduleJsonPath);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
...module,
|
|
68
|
+
extensionAbilities: module.extensionAbilities.filter(extension => (
|
|
69
|
+
typeof extension?.srcEntry !== 'string'
|
|
70
|
+
|| !stale.has(path.posix.join(directory, extension.srcEntry))
|
|
71
|
+
)),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function removeStaleResources(results, kind, plugins) {
|
|
76
|
+
for (const plugin of plugins) {
|
|
77
|
+
for (const [scope, values] of Object.entries(plugin.resources?.[kind] || {})) {
|
|
78
|
+
const names = values as readonly string[];
|
|
79
|
+
const resource = results[scope];
|
|
80
|
+
|
|
81
|
+
if (!resource || typeof resource !== 'object') continue;
|
|
82
|
+
|
|
83
|
+
if (kind === 'media') {
|
|
84
|
+
for (const name of Object.keys(resource)) {
|
|
85
|
+
if (names.includes(path.parse(name).name)) delete resource[name];
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
const field = kind === 'colors' ? 'color' : 'string';
|
|
89
|
+
resource[field] = (resource[field] || []).filter(item => !names.includes(item?.name));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
findStaleConfigPlugins,
|
|
99
|
+
readPreviousCngManifestAsync,
|
|
100
|
+
removeStaleExtensionAbilities,
|
|
101
|
+
removeStalePluginFilesAsync,
|
|
102
|
+
removeStaleResources,
|
|
103
|
+
};
|