diff --git a/documentation/tools/manifest.md b/documentation/tools/manifest.md
index bc7149a9be..6ad96d0ff8 100644
--- a/documentation/tools/manifest.md
+++ b/documentation/tools/manifest.md
@@ -23,6 +23,7 @@ A manifest is a JSON file that describes the modules and resources necessary to
* [`platforms`](#platforms)
* [`subplatforms`](#subplatforms)
* [`bundle`](#bundle)
+ * [`redirect`](#redirect)
* [How manifests are processed](#process)
@@ -706,6 +707,91 @@ The `bundle` object is used by the [`mcbundle` command line tool](./tools.md#mcb
"icon": "./store/icon.png"
}
```
+***
+
+
+### `redirect`
+
+The `redirect` array can be used to redirect (or change) manifest properties that are defined in other manifest files. This is useful when including system manifests where properties need to be altered without editing the Moddable core files.
+
+For example, to use a private directory for sub-modules, you can use an `include` redirection like this:
+
+```json
+"redirect": {
+ "include": {
+ "from": "./targets/$(SUBPLATFORM)/manifest.json",
+ "to": "$(SRC)/targets/$(PLATFORM)/$(SUBPLATFORM)/manifest.json"
+ }
+}
+```
+
+The properties `include`, `strip`, and `preload` (which are string arrays in the manifest) expect `from` and `to` for each property to redirect. The redirection rule can be a single object, or an array of objects:
+
+```json
+"redirect": {
+ "include": [
+ {
+ "from": "some-include-path",
+ "to": "some-redirect-path"
+ },
+ {
+ "from": "another-include-path",
+ "to": "new-path"
+ }
+ ]
+}
+```
+
+The `modules`, `resources`, `data`, `build`, and `config` properties (which are objects in the manifest) expect a similar format, but with an object key qualifier:
+
+```json
+"redirect": {
+ "modules": {
+ "embedded:provider/builtin": {
+ "from": "./targets/$(SUBPLATFORM)/manifest.json",
+ "to": "$(SRC)/targets/$(SUBPLATFORM)/manifest.json"
+ }
+ }
+}
+```
+
+The value of `null` can be used as a wildcard on `from` to match all. For example, to disable all preloads and replace them with a specific list of modules:
+
+```json
+"redirect": {
+ "preload": {
+ "from": null,
+ "to": ["engine", "unit-test"]
+ }
+}
+```
+
+`to` can also use `null` to indicate the item should be deleted (if `to` is not provided, it is assumed to be a delete). For example, to remove all preloads:
+
+```json
+"redirect": {
+ "preload": {
+ "from": null
+ }
+}
+```
+
+You can qualify `redirect` to apply only for specific platforms by placing it inside the `platforms` property. The following will replace the `config.rotation` value only for the `esp32/m5stick_cplus` platform:
+
+```json
+"platforms": {
+ "esp32/m5stick_cplus": {
+ "redirect": {
+ "config": {
+ "rotation": {
+ "from": "270",
+ "to": "90"
+ }
+ }
+ }
+ }
+}
+```
***
diff --git a/tools/mcmanifest.js b/tools/mcmanifest.js
index 628c159a59..846cf46392 100644
--- a/tools/mcmanifest.js
+++ b/tools/mcmanifest.js
@@ -1631,6 +1631,7 @@ export class Tool extends TOOL {
this.windows = this.currentPlatform == "win";
this.slash = this.windows ? "\\" : "/";
this.escapedHash = this.windows ? "^#" : "\\#";
+ this.redirectRules = [];
this.buildPath = this.moddablePath + this.slash + "build";
this.xsPath = this.moddablePath + this.slash + "xs";
@@ -2186,6 +2187,7 @@ export class Tool extends TOOL {
}
parseManifest(path, manifest) {
let platformInclude;
+
if (!manifest) {
var buffer = this.readFileString(path);
try {
@@ -2217,6 +2219,9 @@ export class Tool extends TOOL {
manifest.include = manifest.include.concat(platformInclude);
}
}
+ if ("redirect" in platform) {
+ this.redirectRules = this.redirectRules.concat(platform.redirect);
+ }
if (platform.dependency && ("esp32" == this.platform)) {
manifest.dependencies = [];
for (let i=0; i this.includeManifest(include));
@@ -2237,6 +2246,79 @@ export class Tool extends TOOL {
this.manifests.push(manifest);
return manifest;
}
+ redirect(manifest) {
+ let platform;
+ if ("platforms" in manifest)
+ platform = this.matchPlatform(manifest.platforms, this.fullplatform, false);
+ for (const redirectRule of this.redirectRules) {
+ for (const manifestProperty of Object.keys(redirectRule)) {
+ if (!Array.isArray(redirectRule[manifestProperty]))
+ redirectRule[manifestProperty] = [redirectRule[manifestProperty]];
+ if (["include", "strip", "preload"].includes(manifestProperty)) {
+ for (const singleRule of redirectRule[manifestProperty]) {
+ if (singleRule === null || !("from" in singleRule))
+ throw new Error(`Missing "from" in redirect rule for property "${manifestProperty}"`);
+ this.redirectArray(manifest, manifestProperty, singleRule.from , singleRule.to);
+ if (platform)
+ this.redirectArray(platform, manifestProperty, singleRule.from, singleRule.to);
+ }
+ } else if (["modules", "resources", "data", "build", "config"].includes(manifestProperty)) {
+ for (const keyedRule of redirectRule[manifestProperty]) {
+ if (keyedRule === null || "object" !== typeof keyedRule)
+ throw new Error(`Invalid redirect rule "${manifestProperty}" (must be object, not ${typeof keyedRule})`);
+ for (const singleRuleKey of Object.keys(keyedRule)) {
+ const singleRule = keyedRule[singleRuleKey];
+ if (singleRule === null || "object" !== typeof singleRule)
+ throw new Error(`Invalid redirect rule "${manifestProperty}.${singleRuleKey}" (must be object, not ${typeof singleRule})`);
+ if (!("from" in singleRule))
+ throw new Error(`Missing "from" in redirect rule for property "${manifestProperty}"`);
+ this.redirectObject(manifest, manifestProperty, singleRuleKey, keyedRule[singleRuleKey].from, keyedRule[singleRuleKey].to);
+ if (platform)
+ this.redirectObject(platform, manifestProperty, singleRuleKey, keyedRule[singleRuleKey].from, keyedRule[singleRuleKey].to);
+ }
+ }
+ }
+ else
+ throw new Error(`Redirect not supported for property "${manifestProperty}"`);
+ }
+ }
+ }
+ redirectArray(manifest, property, from, to) {
+ if (!from) {
+ if (property in manifest) {
+ manifest[property] = to ?? [];
+ }
+ } else if ("string" == typeof from) {
+ if (property in manifest) {
+ let array = Array.isArray(manifest[property]) ? manifest[property] : [manifest[property]];
+ if (array.includes(from)) {
+ if (to) {
+ if (!Array.isArray(manifest[property]))
+ manifest[property] = [manifest[property]];
+ manifest[property] = manifest[property].filter(item => item != from).concat(to);
+ } else {
+ if (Array.isArray(manifest[property]) && manifest[property].length > 1)
+ manifest[property] = manifest[property].filter(item => item != from);
+ else
+ delete manifest[property];
+ }
+ }
+ }
+ } else
+ throw new Error(`Invalid redirect rule "${property}.from" (must be string or null, not ${typeof from})`);
+ }
+ redirectObject(manifest, property, key, from, to) {
+ if (!from) {
+ if (property in manifest) {
+ manifest[property] = to ?? {};
+ }
+ } else if ("string" === typeof from) {
+ if (property in manifest) {
+ this.redirectArray(manifest[property], key, from, to);
+ }
+ } else
+ throw new Error(`Invalid redirect rule "${property}.${key}" (must be object or null, not ${typeof from})`);
+ }
resolvePrefix(value) {
const colon = value.indexOf(":");
if (colon > 0) {
@@ -2302,6 +2384,7 @@ export class Tool extends TOOL {
this.manifests.forEach(manifest => this.mergeManifest(this.manifest, manifest));
this.mergeDependencies(this.manifests);
+ this.redirect(this.manifest);
if (this.manifest.errors.length) {
this.manifest.errors.forEach(error => { this.reportError(null, 0, error); });