@expo-harmony/config-plugins 55.0.10-harmony.2 → 55.0.10-harmony.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.
- package/README.md +203 -11
- package/build/compiler.d.ts +7 -0
- package/build/compiler.js +12 -0
- package/build/composition.d.ts +24 -0
- package/build/composition.js +11 -0
- package/build/config.d.ts +2 -15
- package/build/customFiles.d.ts +15 -0
- package/build/customFiles.js +108 -0
- package/build/files.d.ts +1 -1
- package/build/files.js +2 -2
- package/build/generatedFiles.d.ts +15 -0
- package/build/generatedFiles.js +70 -0
- package/build/index.d.ts +20 -5
- package/build/index.js +52 -6
- package/build/internal.d.ts +3 -0
- package/build/internal.js +13 -0
- package/build/manifest.d.ts +38 -0
- package/build/manifest.js +71 -0
- package/build/mods.d.ts +36 -31
- package/build/mods.js +114 -86
- package/build/normalizeConfig.d.ts +10 -3
- package/build/normalizeConfig.js +41 -47
- package/build/ownership.d.ts +4 -3
- package/build/ownership.js +17 -0
- package/build/paths.d.ts +12 -10
- package/build/paths.js +31 -31
- package/build/permissions.d.ts +12 -0
- package/build/permissions.js +60 -0
- package/build/pluginTypes.d.ts +15 -0
- package/build/pluginTypes.js +2 -0
- package/build/resources.d.ts +13 -0
- package/build/resources.js +55 -0
- package/package.json +9 -9
- package/src/compiler.ts +21 -0
- package/src/composition.ts +45 -0
- package/src/config.ts +3 -19
- package/src/customFiles.ts +142 -0
- package/src/files.ts +3 -4
- package/src/generatedFiles.ts +85 -0
- package/src/index.ts +19 -11
- package/src/internal.ts +3 -0
- package/src/manifest.ts +151 -0
- package/src/mods.ts +170 -129
- package/src/nativeInputs.ts +0 -1
- package/src/normalizeConfig.ts +51 -59
- package/src/ownership.ts +25 -5
- package/src/paths.ts +38 -46
- package/src/permissions.ts +68 -0
- package/src/pluginTypes.ts +24 -0
- package/src/resources.ts +65 -0
package/build/paths.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ export interface HarmonyManagedPaths {
|
|
|
3
3
|
readonly projectBuildProfile: string;
|
|
4
4
|
readonly rootOhPackage: string;
|
|
5
5
|
readonly rootHvigor: string;
|
|
6
|
-
readonly nativeInputsStamp: string;
|
|
7
6
|
readonly hvigorConfig: string;
|
|
8
7
|
readonly entryBuildProfile: string;
|
|
9
8
|
readonly entryOhPackage: string;
|
|
@@ -45,18 +44,21 @@ export interface HarmonyPathsApi {
|
|
|
45
44
|
readonly RESOURCE_PATHS: HarmonyResourcePaths;
|
|
46
45
|
assertNoExternalSymlink(root: string, target: string): Promise<void>;
|
|
47
46
|
isInside(root: string, target: string): boolean;
|
|
48
|
-
resolveHarmonyPath(
|
|
49
|
-
resolveProjectPath(
|
|
50
|
-
toPosixRelative(
|
|
47
|
+
resolveHarmonyPath(directory: string, relative: string): Promise<string>;
|
|
48
|
+
resolveProjectPath(root: string, name: keyof HarmonyProjectPaths): Promise<string>;
|
|
49
|
+
toPosixRelative(root: string, target: string): string;
|
|
51
50
|
}
|
|
52
|
-
declare const ManagedPaths: HarmonyManagedPaths
|
|
51
|
+
declare const ManagedPaths: HarmonyManagedPaths & {
|
|
52
|
+
readonly nativeInputsStamp: string;
|
|
53
|
+
};
|
|
53
54
|
declare const ProjectPaths: HarmonyProjectPaths;
|
|
54
55
|
declare const ProjectPathCandidates: HarmonyProjectPathCandidates;
|
|
55
56
|
declare const ResourcePaths: HarmonyResourcePaths;
|
|
56
57
|
export declare function isInside(root: string, target: string): boolean;
|
|
57
58
|
export declare function assertNoExternalSymlink(root: string, target: string): Promise<void>;
|
|
58
|
-
export declare function resolveHarmonyPath(
|
|
59
|
-
export declare function resolveProjectPath(
|
|
60
|
-
export declare function toPosixRelative(
|
|
61
|
-
|
|
62
|
-
export
|
|
59
|
+
export declare function resolveHarmonyPath(directory: string, relative: string): Promise<string>;
|
|
60
|
+
export declare function resolveProjectPath(root: string, name: keyof HarmonyProjectPaths): Promise<string>;
|
|
61
|
+
export declare function toPosixRelative(root: string, target: string): string;
|
|
62
|
+
declare const NativeInputsStampPath: string;
|
|
63
|
+
export declare const HarmonyPaths: Readonly<HarmonyPathsApi>;
|
|
64
|
+
export { ManagedPaths, NativeInputsStampPath, ProjectPathCandidates, ProjectPaths, ResourcePaths };
|
package/build/paths.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.ResourcePaths = exports.ProjectPaths = exports.ProjectPathCandidates = exports.ManagedPaths = exports.HarmonyPaths = void 0;
|
|
6
|
+
exports.ResourcePaths = exports.ProjectPaths = exports.ProjectPathCandidates = exports.NativeInputsStampPath = exports.ManagedPaths = exports.HarmonyPaths = void 0;
|
|
7
7
|
exports.isInside = isInside;
|
|
8
8
|
exports.assertNoExternalSymlink = assertNoExternalSymlink;
|
|
9
9
|
exports.resolveHarmonyPath = resolveHarmonyPath;
|
|
@@ -37,8 +37,7 @@ const ProjectPaths = Object.freeze({
|
|
|
37
37
|
});
|
|
38
38
|
exports.ProjectPaths = ProjectPaths;
|
|
39
39
|
const ProjectPathCandidates = Object.freeze({
|
|
40
|
-
//
|
|
41
|
-
// search places. The first entry is also the file created for new projects.
|
|
40
|
+
// Match React Native CLI lookup precedence; new projects use the first path.
|
|
42
41
|
reactNativeConfig: Object.freeze([
|
|
43
42
|
'react-native.config.js',
|
|
44
43
|
'react-native.config.cjs',
|
|
@@ -69,55 +68,56 @@ function isInside(root, target) {
|
|
|
69
68
|
|| (!relative.startsWith(`..${node_path_1.default.sep}`) && relative !== '..' && !node_path_1.default.isAbsolute(relative));
|
|
70
69
|
}
|
|
71
70
|
async function assertNoExternalSymlink(root, target) {
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const relative = node_path_1.default.relative(
|
|
75
|
-
let
|
|
71
|
+
const absolute = node_path_1.default.resolve(root);
|
|
72
|
+
const canonical = await node_fs_1.default.promises.realpath(absolute).catch(() => absolute);
|
|
73
|
+
const relative = node_path_1.default.relative(absolute, target);
|
|
74
|
+
let current = absolute;
|
|
76
75
|
for (const segment of relative.split(node_path_1.default.sep).filter(Boolean)) {
|
|
77
|
-
|
|
76
|
+
current = node_path_1.default.join(current, segment);
|
|
78
77
|
let stat;
|
|
79
78
|
try {
|
|
80
|
-
stat = await node_fs_1.default.promises.lstat(
|
|
79
|
+
stat = await node_fs_1.default.promises.lstat(current);
|
|
81
80
|
}
|
|
82
81
|
catch (cause) {
|
|
83
82
|
if (cause.code === 'ENOENT')
|
|
84
83
|
break;
|
|
85
|
-
throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_PATH_INVALID', `Cannot inspect managed Harmony path ${
|
|
84
|
+
throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_PATH_INVALID', `Cannot inspect managed Harmony path ${current}: ${cause.message}`, { cause, file: current, operation: 'resolve-path' });
|
|
86
85
|
}
|
|
87
86
|
if (stat.isSymbolicLink()) {
|
|
88
|
-
const real = await node_fs_1.default.promises.realpath(
|
|
89
|
-
if (!isInside(
|
|
90
|
-
throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_PATH_ESCAPE', `Refusing to follow a symlink outside the Harmony project: ${
|
|
87
|
+
const real = await node_fs_1.default.promises.realpath(current);
|
|
88
|
+
if (!isInside(canonical, real)) {
|
|
89
|
+
throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_PATH_ESCAPE', `Refusing to follow a symlink outside the Harmony project: ${current}`, { file: current, operation: 'resolve-path' });
|
|
91
90
|
}
|
|
92
91
|
}
|
|
93
92
|
}
|
|
94
93
|
}
|
|
95
|
-
async function resolveHarmonyPath(
|
|
96
|
-
if (typeof
|
|
97
|
-
throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_PATH_ESCAPE', `Invalid managed path: ${
|
|
94
|
+
async function resolveHarmonyPath(directory, relative) {
|
|
95
|
+
if (typeof relative !== 'string' || !relative || node_path_1.default.isAbsolute(relative)) {
|
|
96
|
+
throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_PATH_ESCAPE', `Invalid managed path: ${relative}`, { operation: 'resolve-path' });
|
|
98
97
|
}
|
|
99
|
-
const root = node_path_1.default.resolve(
|
|
100
|
-
const target = node_path_1.default.resolve(root,
|
|
98
|
+
const root = node_path_1.default.resolve(directory);
|
|
99
|
+
const target = node_path_1.default.resolve(root, relative);
|
|
101
100
|
if (!isInside(root, target)) {
|
|
102
|
-
throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_PATH_ESCAPE', `Managed path escapes the Harmony project: ${
|
|
101
|
+
throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_PATH_ESCAPE', `Managed path escapes the Harmony project: ${relative}`, { file: target, operation: 'resolve-path' });
|
|
103
102
|
}
|
|
104
103
|
await assertNoExternalSymlink(root, target);
|
|
105
104
|
return target;
|
|
106
105
|
}
|
|
107
|
-
async function resolveProjectPath(
|
|
108
|
-
const candidates = ProjectPathCandidates[
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `Multiple files provide harmony.${modName}: ${existing.map(file => node_path_1.default.basename(file)).join(', ')}`, { file: existing[0], operation: `harmony.${modName}.read` });
|
|
106
|
+
async function resolveProjectPath(root, name) {
|
|
107
|
+
const candidates = ProjectPathCandidates[name] || [ProjectPaths[name]];
|
|
108
|
+
for (const candidate of candidates) {
|
|
109
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(root, candidate)))
|
|
110
|
+
return resolveHarmonyPath(root, candidate);
|
|
113
111
|
}
|
|
114
|
-
return
|
|
112
|
+
return resolveHarmonyPath(root, candidates[0]);
|
|
115
113
|
}
|
|
116
|
-
function toPosixRelative(
|
|
117
|
-
return node_path_1.default.relative(
|
|
114
|
+
function toPosixRelative(root, target) {
|
|
115
|
+
return node_path_1.default.relative(root, target).split(node_path_1.default.sep).join('/');
|
|
118
116
|
}
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
const { nativeInputsStamp: NativeInputsStampPath, ...PublicManagedPaths } = ManagedPaths;
|
|
118
|
+
exports.NativeInputsStampPath = NativeInputsStampPath;
|
|
119
|
+
exports.HarmonyPaths = Object.freeze({
|
|
120
|
+
HARMONY_PATHS: Object.freeze(PublicManagedPaths),
|
|
121
121
|
PROJECT_PATH_CANDIDATES: ProjectPathCandidates,
|
|
122
122
|
PROJECT_PATHS: ProjectPaths,
|
|
123
123
|
RESOURCE_PATHS: ResourcePaths,
|
|
@@ -126,4 +126,4 @@ exports.HarmonyPaths = {
|
|
|
126
126
|
resolveHarmonyPath,
|
|
127
127
|
resolveProjectPath,
|
|
128
128
|
toPosixRelative,
|
|
129
|
-
};
|
|
129
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HarmonyPermission } from './config';
|
|
2
|
+
import { type HarmonyModuleJson } from './manifest';
|
|
3
|
+
import type { HarmonyConfigPlugin } from './pluginTypes';
|
|
4
|
+
export declare function mergePermissions(current: readonly HarmonyPermission[], additions: readonly HarmonyPermission[]): HarmonyPermission[];
|
|
5
|
+
export declare function ensurePermissions(manifest: HarmonyModuleJson, permissions: readonly HarmonyPermission[]): HarmonyModuleJson;
|
|
6
|
+
export declare function removePermissions(manifest: HarmonyModuleJson, names: readonly string[]): HarmonyModuleJson;
|
|
7
|
+
export declare const withHarmonyPermissions: HarmonyConfigPlugin<readonly HarmonyPermission[]>;
|
|
8
|
+
export declare const HarmonyPermissions: Readonly<{
|
|
9
|
+
ensurePermissions: typeof ensurePermissions;
|
|
10
|
+
mergePermissions: typeof mergePermissions;
|
|
11
|
+
removePermissions: typeof removePermissions;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HarmonyPermissions = exports.withHarmonyPermissions = void 0;
|
|
4
|
+
exports.mergePermissions = mergePermissions;
|
|
5
|
+
exports.ensurePermissions = ensurePermissions;
|
|
6
|
+
exports.removePermissions = removePermissions;
|
|
7
|
+
const manifest_1 = require("./manifest");
|
|
8
|
+
const mods_1 = require("./mods");
|
|
9
|
+
const normalizeConfig_1 = require("./normalizeConfig");
|
|
10
|
+
function mergePermissions(current, additions) {
|
|
11
|
+
const result = new Map();
|
|
12
|
+
for (const input of [...current, ...additions]) {
|
|
13
|
+
const permission = (0, normalizeConfig_1.normalizePermission)(input);
|
|
14
|
+
const existing = result.get(permission.name);
|
|
15
|
+
const scene = existing?.usedScene || permission.usedScene;
|
|
16
|
+
const abilities = [...new Set([
|
|
17
|
+
...(existing?.usedScene?.abilities ?? []),
|
|
18
|
+
...(permission.usedScene?.abilities ?? []),
|
|
19
|
+
])];
|
|
20
|
+
result.set(permission.name, {
|
|
21
|
+
...existing,
|
|
22
|
+
...permission,
|
|
23
|
+
...(scene
|
|
24
|
+
? {
|
|
25
|
+
usedScene: {
|
|
26
|
+
...(abilities.length ? { abilities } : {}),
|
|
27
|
+
when: existing?.usedScene?.when === 'always' || permission.usedScene?.when === 'always' ? 'always' : 'inuse',
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
: {}),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return [...result.values()];
|
|
34
|
+
}
|
|
35
|
+
function ensurePermissions(manifest, permissions) {
|
|
36
|
+
const module = (0, manifest_1.getModuleOrThrow)(manifest);
|
|
37
|
+
module.requestPermissions = mergePermissions(module.requestPermissions ?? [], permissions);
|
|
38
|
+
return manifest;
|
|
39
|
+
}
|
|
40
|
+
function removePermissions(manifest, names) {
|
|
41
|
+
const module = (0, manifest_1.getModuleOrThrow)(manifest);
|
|
42
|
+
if (module.requestPermissions) {
|
|
43
|
+
module.requestPermissions = module.requestPermissions.filter(permission => !names.includes(permission.name));
|
|
44
|
+
}
|
|
45
|
+
return manifest;
|
|
46
|
+
}
|
|
47
|
+
const withHarmonyPermissions = (config, permissions) => {
|
|
48
|
+
const additions = mergePermissions([], permissions);
|
|
49
|
+
if (config.harmony) {
|
|
50
|
+
config.harmony = { ...config.harmony, permissions: mergePermissions(config.harmony.permissions ?? [], additions) };
|
|
51
|
+
}
|
|
52
|
+
return (0, mods_1.withModuleJson)(config, (mod) => {
|
|
53
|
+
if (mod.modResults.module === undefined)
|
|
54
|
+
mod.modResults.module = {};
|
|
55
|
+
ensurePermissions(mod.modResults, additions);
|
|
56
|
+
return mod;
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
exports.withHarmonyPermissions = withHarmonyPermissions;
|
|
60
|
+
exports.HarmonyPermissions = Object.freeze({ ensurePermissions, mergePermissions, removePermissions });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ExportedConfigWithProps } from '@expo/config-plugins';
|
|
2
|
+
import type { ExpoConfigWithHarmony, ExpoHarmonyPlatform, HarmonyConfig } from './config';
|
|
3
|
+
export type HarmonyConfigPlugin<Props = void> = <Config extends ExpoConfigWithHarmony>(config: Config, props: Props) => Config;
|
|
4
|
+
export type HarmonyModConfig<T> = Omit<ExportedConfigWithProps<T>, 'platforms' | 'modRawConfig' | 'modRequest'> & {
|
|
5
|
+
platforms?: ExpoHarmonyPlatform[];
|
|
6
|
+
harmony?: HarmonyConfig;
|
|
7
|
+
modRawConfig: ExpoConfigWithHarmony;
|
|
8
|
+
modRequest: Omit<ExportedConfigWithProps<T>['modRequest'], 'platform' | 'nextMod'> & {
|
|
9
|
+
platform: 'harmony';
|
|
10
|
+
nextMod?: HarmonyModAction<T>;
|
|
11
|
+
modFile?: string;
|
|
12
|
+
modFileExists?: boolean;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export type HarmonyModAction<T = unknown> = (config: HarmonyModConfig<T>) => HarmonyModConfig<T> | Promise<HarmonyModConfig<T>>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface HarmonyResourceItem extends Record<string, unknown> {
|
|
2
|
+
name: string;
|
|
3
|
+
value: string;
|
|
4
|
+
}
|
|
5
|
+
export interface HarmonyResourceFile extends Record<string, unknown> {
|
|
6
|
+
string?: HarmonyResourceItem[];
|
|
7
|
+
color?: HarmonyResourceItem[];
|
|
8
|
+
}
|
|
9
|
+
export declare function setString(file: HarmonyResourceFile, item: HarmonyResourceItem): HarmonyResourceFile;
|
|
10
|
+
export declare function removeString(file: HarmonyResourceFile, name: string): HarmonyResourceFile;
|
|
11
|
+
export declare function setColor(file: HarmonyResourceFile, item: HarmonyResourceItem): HarmonyResourceFile;
|
|
12
|
+
export declare function removeColor(file: HarmonyResourceFile, name: string): HarmonyResourceFile;
|
|
13
|
+
export declare function toArgb(value: string | number): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setString = setString;
|
|
4
|
+
exports.removeString = removeString;
|
|
5
|
+
exports.setColor = setColor;
|
|
6
|
+
exports.removeColor = removeColor;
|
|
7
|
+
exports.toArgb = toArgb;
|
|
8
|
+
const errors_1 = require("./errors");
|
|
9
|
+
function setItem(file, kind, item) {
|
|
10
|
+
if (!item || typeof item.name !== 'string' || !/^[A-Za-z0-9_.-]+$/.test(item.name)
|
|
11
|
+
|| item.name.includes('..') || typeof item.value !== 'string') {
|
|
12
|
+
throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_RESOURCE_INVALID', `Invalid Harmony ${kind} resource.`, { operation: 'edit-resource' });
|
|
13
|
+
}
|
|
14
|
+
const items = readItems(file, kind);
|
|
15
|
+
const existing = items.find(value => value?.name === item.name);
|
|
16
|
+
file[kind] = [...items.filter(value => value?.name !== item.name), { ...existing, ...item }];
|
|
17
|
+
return file;
|
|
18
|
+
}
|
|
19
|
+
function readItems(file, kind) {
|
|
20
|
+
if (file[kind] !== undefined && !Array.isArray(file[kind])) {
|
|
21
|
+
throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_RESOURCE_INVALID', `Harmony ${kind} resources must be an array.`, { operation: 'edit-resource' });
|
|
22
|
+
}
|
|
23
|
+
return file[kind] ?? [];
|
|
24
|
+
}
|
|
25
|
+
function setString(file, item) {
|
|
26
|
+
return setItem(file, 'string', item);
|
|
27
|
+
}
|
|
28
|
+
function removeString(file, name) {
|
|
29
|
+
const items = readItems(file, 'string');
|
|
30
|
+
if (file.string)
|
|
31
|
+
file.string = items.filter(item => item?.name !== name);
|
|
32
|
+
return file;
|
|
33
|
+
}
|
|
34
|
+
function setColor(file, item) {
|
|
35
|
+
return setItem(file, 'color', item);
|
|
36
|
+
}
|
|
37
|
+
function removeColor(file, name) {
|
|
38
|
+
const items = readItems(file, 'color');
|
|
39
|
+
if (file.color)
|
|
40
|
+
file.color = items.filter(item => item?.name !== name);
|
|
41
|
+
return file;
|
|
42
|
+
}
|
|
43
|
+
function toArgb(value) {
|
|
44
|
+
let hex;
|
|
45
|
+
if (typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 0xffffffff) {
|
|
46
|
+
hex = value.toString(16).padStart(8, '0');
|
|
47
|
+
}
|
|
48
|
+
else if (typeof value === 'string' && /^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/i.test(value)) {
|
|
49
|
+
hex = value.slice(1);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', 'Color must be #RRGGBB, #RRGGBBAA or an unsigned RGBA integer.', { operation: 'convert-color' });
|
|
53
|
+
}
|
|
54
|
+
return `#${hex.length === 8 ? hex.slice(6, 8) + hex.slice(0, 6) : hex}`.toUpperCase();
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo-harmony/config-plugins",
|
|
3
|
-
"version": "55.0.10-harmony.
|
|
3
|
+
"version": "55.0.10-harmony.3",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"react-native",
|
|
6
6
|
"expo",
|
|
@@ -29,12 +29,17 @@
|
|
|
29
29
|
"require": "./build/index.js",
|
|
30
30
|
"default": "./build/index.js"
|
|
31
31
|
},
|
|
32
|
-
"./
|
|
32
|
+
"./package.json": "./package.json",
|
|
33
|
+
"./internal": {
|
|
34
|
+
"types": "./build/internal.d.ts",
|
|
35
|
+
"require": "./build/internal.js",
|
|
36
|
+
"default": "./build/internal.js"
|
|
37
|
+
},
|
|
38
|
+
"./internal/native-inputs": {
|
|
33
39
|
"types": "./build/nativeInputs.d.ts",
|
|
34
40
|
"require": "./build/nativeInputs.js",
|
|
35
41
|
"default": "./build/nativeInputs.js"
|
|
36
|
-
}
|
|
37
|
-
"./package.json": "./package.json"
|
|
42
|
+
}
|
|
38
43
|
},
|
|
39
44
|
"dependencies": {
|
|
40
45
|
"@expo/config-plugins": "55.0.10",
|
|
@@ -44,11 +49,6 @@
|
|
|
44
49
|
"peerDependencies": {
|
|
45
50
|
"@expo/config": "55.0.17"
|
|
46
51
|
},
|
|
47
|
-
"peerDependenciesMeta": {
|
|
48
|
-
"@expo/config": {
|
|
49
|
-
"optional": true
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/node": "18.19.0",
|
|
54
54
|
"typescript": "^6.0.3"
|
package/src/compiler.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { compileModsAsync, type ExportedConfig, type ModPlatform } from '@expo/config-plugins';
|
|
2
|
+
|
|
3
|
+
import type { ExpoConfigWithHarmony } from './config';
|
|
4
|
+
import { withHarmonyBaseMods } from './mods';
|
|
5
|
+
|
|
6
|
+
export interface HarmonyCompileOptions {
|
|
7
|
+
projectRoot: string;
|
|
8
|
+
introspect?: boolean;
|
|
9
|
+
ignoreExistingNativeFiles?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function compileHarmonyModsAsync<Config extends ExpoConfigWithHarmony>(
|
|
13
|
+
config: Config,
|
|
14
|
+
options: HarmonyCompileOptions
|
|
15
|
+
): Promise<Config> {
|
|
16
|
+
return await compileModsAsync(withHarmonyBaseMods(config) as ExportedConfig, {
|
|
17
|
+
...options,
|
|
18
|
+
platforms: ['harmony' as ModPlatform],
|
|
19
|
+
assertMissingModProviders: true,
|
|
20
|
+
}) as Config;
|
|
21
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRunOncePlugin as expoCreateRunOncePlugin,
|
|
3
|
+
withPlugins as expoWithPlugins,
|
|
4
|
+
withRunOnce as expoWithRunOnce,
|
|
5
|
+
withStaticPlugin as expoWithStaticPlugin,
|
|
6
|
+
type ConfigPlugin,
|
|
7
|
+
} from '@expo/config-plugins';
|
|
8
|
+
|
|
9
|
+
import type { HarmonyConfigPlugin } from './pluginTypes';
|
|
10
|
+
import type { ExpoConfigWithHarmony } from './config';
|
|
11
|
+
|
|
12
|
+
// Expo plugins accept the narrower platform union. Method variance allows them
|
|
13
|
+
// alongside Harmony plugins without losing contextual types for inline callbacks.
|
|
14
|
+
type CompatiblePlugin<Props = void> = {
|
|
15
|
+
apply(config: ExpoConfigWithHarmony, props: Props): ExpoConfigWithHarmony;
|
|
16
|
+
}['apply'];
|
|
17
|
+
|
|
18
|
+
export type HarmonyPluginReference = string | CompatiblePlugin<never>;
|
|
19
|
+
export type HarmonyStaticPlugin = HarmonyPluginReference | [HarmonyPluginReference, unknown];
|
|
20
|
+
|
|
21
|
+
export const withPlugins = expoWithPlugins as unknown as HarmonyConfigPlugin<HarmonyStaticPlugin[]>;
|
|
22
|
+
export const withRunOnce = expoWithRunOnce as unknown as HarmonyConfigPlugin<{
|
|
23
|
+
plugin: CompatiblePlugin;
|
|
24
|
+
name: string;
|
|
25
|
+
version?: string;
|
|
26
|
+
}>;
|
|
27
|
+
export const withStaticPlugin = expoWithStaticPlugin as unknown as HarmonyConfigPlugin<{
|
|
28
|
+
plugin: HarmonyStaticPlugin;
|
|
29
|
+
projectRoot?: string;
|
|
30
|
+
fallback?: CompatiblePlugin<{ _resolverError: Error } & Record<string, unknown>>;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
export function createRunOncePlugin<Props>(
|
|
34
|
+
plugin: HarmonyConfigPlugin<Props>, name: string, version?: string
|
|
35
|
+
): HarmonyConfigPlugin<Props>;
|
|
36
|
+
export function createRunOncePlugin<Props>(
|
|
37
|
+
plugin: ConfigPlugin<Props>, name: string, version?: string
|
|
38
|
+
): HarmonyConfigPlugin<Props>;
|
|
39
|
+
export function createRunOncePlugin(
|
|
40
|
+
plugin: ConfigPlugin<never> | HarmonyConfigPlugin<never>,
|
|
41
|
+
name: string,
|
|
42
|
+
version?: string
|
|
43
|
+
): HarmonyConfigPlugin<never> {
|
|
44
|
+
return expoCreateRunOncePlugin(plugin as ConfigPlugin<never>, name, version) as HarmonyConfigPlugin<never>;
|
|
45
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -18,25 +18,14 @@ export interface HarmonyPermission {
|
|
|
18
18
|
usedScene?: { abilities?: string[]; when?: 'inuse' | 'always' };
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export type HarmonySkillUri = Record<string, string | number | boolean>;
|
|
22
|
+
|
|
21
23
|
export interface HarmonySkill {
|
|
22
24
|
entities?: string[];
|
|
23
25
|
actions?: string[];
|
|
24
|
-
uris?:
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface HarmonyFontDefinition {
|
|
28
|
-
path: string;
|
|
29
|
-
weight?: number;
|
|
30
|
-
style?: 'normal' | 'italic';
|
|
26
|
+
uris?: HarmonySkillUri[];
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
export type HarmonyFont
|
|
34
|
-
= | string
|
|
35
|
-
| {
|
|
36
|
-
fontFamily: string;
|
|
37
|
-
fontDefinitions: HarmonyFontDefinition[];
|
|
38
|
-
};
|
|
39
|
-
|
|
40
29
|
export interface HarmonyConfig {
|
|
41
30
|
bundleName: string;
|
|
42
31
|
moduleName?: string;
|
|
@@ -46,14 +35,11 @@ export interface HarmonyConfig {
|
|
|
46
35
|
versionCode?: number;
|
|
47
36
|
versionName?: string;
|
|
48
37
|
targetApiVersion?: number;
|
|
49
|
-
/** Harmony SDK label written to build-profile.json5, e.g. 6.0.0(20). Its API must match targetApiVersion. */
|
|
50
38
|
targetSdkVersion?: number | string;
|
|
51
|
-
/** API number, or a full Harmony SDK label ending in the API number. */
|
|
52
39
|
compatibleSdkVersion?: number | string;
|
|
53
40
|
deviceTypes?: HarmonyDeviceType[];
|
|
54
41
|
permissions?: HarmonyPermission[];
|
|
55
42
|
skills?: HarmonySkill[];
|
|
56
|
-
/** URL schemes the app may inspect with Linking.canOpenURL. http, https, and expo.scheme are added automatically. */
|
|
57
43
|
querySchemes?: string[];
|
|
58
44
|
icon?: string;
|
|
59
45
|
label?: string;
|
|
@@ -63,8 +49,6 @@ export interface HarmonyConfig {
|
|
|
63
49
|
jsEngine?: 'hermes';
|
|
64
50
|
abiFilters?: string[];
|
|
65
51
|
signingConfigFile?: string;
|
|
66
|
-
/** Fonts bundled when the `@expo-harmony/expo-font` config plugin is registered. */
|
|
67
|
-
fonts?: HarmonyFont[];
|
|
68
52
|
}
|
|
69
53
|
|
|
70
54
|
export type ExpoConfigWithHarmony = Omit<ExpoConfig, 'platforms'> & {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import { withBaseMod, type ExportedConfig, type ModPlatform } from '@expo/config-plugins';
|
|
4
|
+
import JSON5 from 'json5';
|
|
5
|
+
|
|
6
|
+
import { HarmonyConfigPluginError } from './errors';
|
|
7
|
+
import { atomicWrite, stableJson } from './files';
|
|
8
|
+
import { recordManagedFile, registerMod } from './mods';
|
|
9
|
+
import { getHarmonyConfigPlugins, normalizeHarmonyConfigPlugins, registerHarmonyConfigPlugin } from './ownership';
|
|
10
|
+
import { ManagedPaths, ResourcePaths, resolveHarmonyPath } from './paths';
|
|
11
|
+
import type { HarmonyConfigPlugin, HarmonyModAction, HarmonyModConfig } from './pluginTypes';
|
|
12
|
+
|
|
13
|
+
export interface HarmonyFileModOptions<T> {
|
|
14
|
+
path: string;
|
|
15
|
+
owner: string;
|
|
16
|
+
parse: (source: string | null) => T;
|
|
17
|
+
serialize: (value: T) => string;
|
|
18
|
+
isIntrospective?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Definition = Omit<HarmonyFileModOptions<unknown>, 'parse' | 'serialize'> & { parse: unknown; serialize: unknown };
|
|
22
|
+
interface CustomProvider { expoHarmonyLateActions?: unknown }
|
|
23
|
+
const providers = new WeakMap<object, Definition>();
|
|
24
|
+
|
|
25
|
+
export function createHarmonyFileMod<T>(options: HarmonyFileModOptions<T>): HarmonyConfigPlugin<HarmonyModAction<T>> {
|
|
26
|
+
const definition = Object.freeze({ ...options });
|
|
27
|
+
const { path: relative, owner, parse, serialize, isIntrospective = false } = definition;
|
|
28
|
+
|
|
29
|
+
normalizeHarmonyConfigPlugins([{ owner, files: [relative] }]);
|
|
30
|
+
|
|
31
|
+
const reserved = Object.values(ManagedPaths).includes(relative)
|
|
32
|
+
|| Object.values(ResourcePaths).some(scopes => Object.values(scopes).some(file => relative === file))
|
|
33
|
+
|| Object.values(ResourcePaths.media).some(directory => relative.startsWith(`${directory}/`));
|
|
34
|
+
if (reserved || relative.split('/').some(part => part === '.git') || relative.startsWith('.expo/')) {
|
|
35
|
+
throw new HarmonyConfigPluginError('ERR_HARMONY_PATH_INVALID', `Use the built-in mod for reserved path ${relative}.`);
|
|
36
|
+
}
|
|
37
|
+
if (typeof parse !== 'function' || typeof serialize !== 'function') {
|
|
38
|
+
throw new TypeError('A file mod requires parse and serialize functions.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const name = `file:${relative}`;
|
|
42
|
+
|
|
43
|
+
return (config, action) => {
|
|
44
|
+
const mods = (config as ExportedConfig).mods as Record<string, Record<string, CustomProvider>> | undefined;
|
|
45
|
+
const provider = mods?.harmony?.[name];
|
|
46
|
+
if (provider) {
|
|
47
|
+
const stored = providers.get(provider);
|
|
48
|
+
if (!stored || stored.owner !== owner || stored.parse !== parse
|
|
49
|
+
|| stored.serialize !== serialize || !!stored.isIntrospective !== isIntrospective) {
|
|
50
|
+
throw new HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `Conflicting providers for ${relative}. Reuse the same file mod definition.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const claims = getHarmonyConfigPlugins(config).find(plugin => plugin.owner === owner);
|
|
55
|
+
registerHarmonyConfigPlugin(config, owner, {
|
|
56
|
+
...claims, files: [...new Set([...(claims?.files ?? []), relative])],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!provider) {
|
|
60
|
+
const actions: HarmonyModAction<T>[] = [];
|
|
61
|
+
withBaseMod<T>(config as ExportedConfig, {
|
|
62
|
+
platform: 'harmony' as ModPlatform,
|
|
63
|
+
mod: name,
|
|
64
|
+
isProvider: true,
|
|
65
|
+
isIntrospective,
|
|
66
|
+
saveToInternal: isIntrospective,
|
|
67
|
+
async action(value) {
|
|
68
|
+
const { nextMod: _next, ...request } = value.modRequest;
|
|
69
|
+
const file = await resolveHarmonyPath(request.platformProjectRoot, relative);
|
|
70
|
+
let source: string | null = null;
|
|
71
|
+
|
|
72
|
+
if (!request.ignoreExistingNativeFiles) {
|
|
73
|
+
try {
|
|
74
|
+
source = await fs.readFile(file, 'utf8');
|
|
75
|
+
} catch (cause) {
|
|
76
|
+
if ((cause as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
77
|
+
throw new HarmonyConfigPluginError(
|
|
78
|
+
'ERR_HARMONY_TEXT_INVALID',
|
|
79
|
+
`Cannot read ${file}: ${(cause as Error).message}`,
|
|
80
|
+
{ cause, file, operation: `harmony.${name}.read` }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let result = {
|
|
87
|
+
...value,
|
|
88
|
+
modRequest: { ...request, modFile: file, modFileExists: source !== null },
|
|
89
|
+
modResults: parse(source),
|
|
90
|
+
} as unknown as HarmonyModConfig<T>;
|
|
91
|
+
for (let index = actions.length - 1; index >= 0; index--) {
|
|
92
|
+
result = await actions[index](result) as HarmonyModConfig<T>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const output = serialize(result.modResults);
|
|
96
|
+
if (typeof output !== 'string') throw new TypeError(`Serializer for ${relative} must return a string.`);
|
|
97
|
+
|
|
98
|
+
if (!request.introspect) {
|
|
99
|
+
await atomicWrite(file, output);
|
|
100
|
+
recordManagedFile(result, file, owner);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result as unknown as typeof value;
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const table = ((config as ExportedConfig).mods as Record<string, Record<string, CustomProvider>>).harmony;
|
|
108
|
+
table[name].expoHarmonyLateActions = actions;
|
|
109
|
+
providers.set(table[name], definition);
|
|
110
|
+
|
|
111
|
+
// CNG must fingerprint custom outputs after they have been written.
|
|
112
|
+
for (const stage of ['patch', 'manifest']) {
|
|
113
|
+
if (table[stage]) {
|
|
114
|
+
const provider = table[stage];
|
|
115
|
+
delete table[stage];
|
|
116
|
+
table[stage] = provider;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return registerMod(config, name, action);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseJson(source: string | null): Record<string, unknown> {
|
|
126
|
+
const value = source === null ? {} : JSON5.parse(source);
|
|
127
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
128
|
+
throw new TypeError('A Harmony JSON file must contain an object.');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface HarmonyJsonFileOptions {
|
|
135
|
+
path: string;
|
|
136
|
+
owner: string;
|
|
137
|
+
action: HarmonyModAction<Record<string, unknown>>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const withHarmonyJsonFile: HarmonyConfigPlugin<HarmonyJsonFileOptions> = (config, { path, owner, action }) => (
|
|
141
|
+
createHarmonyFileMod({ path, owner, parse: parseJson, serialize: stableJson, isIntrospective: true })(config, action)
|
|
142
|
+
);
|
package/src/files.ts
CHANGED
|
@@ -21,7 +21,7 @@ export function stableJson(value: unknown): string {
|
|
|
21
21
|
return `${JSON.stringify(sortValue(value), null, 2)}\n`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export async function readJson5<T>(file: string, fallback: T,
|
|
24
|
+
export async function readJson5<T>(file: string, fallback: T, name: string): Promise<T> {
|
|
25
25
|
try {
|
|
26
26
|
const source = await fs.promises.readFile(file, 'utf8');
|
|
27
27
|
|
|
@@ -31,8 +31,8 @@ export async function readJson5<T>(file: string, fallback: T, modName: string):
|
|
|
31
31
|
|
|
32
32
|
throw new HarmonyConfigPluginError(
|
|
33
33
|
'ERR_HARMONY_JSON5_INVALID',
|
|
34
|
-
`Cannot parse ${file} for harmony.${
|
|
35
|
-
{ cause, file, operation: `harmony.${
|
|
34
|
+
`Cannot parse ${file} for harmony.${name}: ${(cause as Error).message}`,
|
|
35
|
+
{ cause, file, operation: `harmony.${name}.read` }
|
|
36
36
|
);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -74,6 +74,5 @@ export async function writeJson5(file: string, value: unknown): Promise<void> {
|
|
|
74
74
|
|
|
75
75
|
export async function sha256File(file: string): Promise<string> {
|
|
76
76
|
const data = Uint8Array.from(await fs.promises.readFile(file));
|
|
77
|
-
|
|
78
77
|
return crypto.createHash('sha256').update(data).digest('hex');
|
|
79
78
|
}
|