@lynxship/cli 0.1.16 → 0.1.18
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 +22 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +15 -0
- package/dist/index.js +106 -1
- package/dist/plugin-api.d.ts +2 -0
- package/dist/plugin-api.d.ts.map +1 -0
- package/dist/plugin-api.js +1 -0
- package/dist/plugins.d.ts +35 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +595 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -25,6 +25,26 @@ npx @lynxship/cli@latest doctor --project-dir ./my-lynx-app
|
|
|
25
25
|
|
|
26
26
|
The package exposes the `lynxship` executable.
|
|
27
27
|
|
|
28
|
+
## Plugin ecosystem
|
|
29
|
+
|
|
30
|
+
Install a community plugin as a normal project dependency, list it in
|
|
31
|
+
`lynxship.json`, then validate it before building:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install --save-dev @example/lynx-plugin
|
|
35
|
+
npx lynxship plugin doctor
|
|
36
|
+
npx lynxship plugin list
|
|
37
|
+
npx lynxship plugin apply --platform android --profile production
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The public contract is exported by `@lynxship/plugin-api`. Plugins can make
|
|
41
|
+
JSON configuration contributions and bounded, idempotent Android/iOS/native
|
|
42
|
+
file changes. Autolink remains the official Lynx `lynx.lib.json` and codegen
|
|
43
|
+
workflow. Template and cloud contributions are discoverable metadata until a
|
|
44
|
+
reviewed provider or template service consumes them.
|
|
45
|
+
|
|
46
|
+
See the full contract in the repository `docs/plugin-ecosystem.md`.
|
|
47
|
+
|
|
28
48
|
## Vue Lynx projects
|
|
29
49
|
|
|
30
50
|
LynxShip supports projects created with the official Vue Lynx scaffold. Vue
|
|
@@ -514,7 +534,8 @@ actual recording and analysis.
|
|
|
514
534
|
|
|
515
535
|
## Package layout
|
|
516
536
|
|
|
517
|
-
The CLI is backed by public `@lynxship/*` runtime packages in this workspace
|
|
537
|
+
The CLI is backed by public `@lynxship/*` runtime packages in this workspace,
|
|
538
|
+
including the public `@lynxship/plugin-api` contract.
|
|
518
539
|
Their published versions may differ from the CLI version; workspace ranges are
|
|
519
540
|
rewritten to released versions when the package is packed. Publish a changed
|
|
520
541
|
runtime package before publishing the CLI when its source or version changed.
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Platform } from "@lynxship/contracts";
|
|
2
|
+
import type { PluginReference } from "@lynxship/plugin-api";
|
|
2
3
|
export interface BuildProfile {
|
|
3
4
|
distribution?: string;
|
|
4
5
|
channel?: string;
|
|
@@ -43,6 +44,7 @@ export interface BuildProfile {
|
|
|
43
44
|
}
|
|
44
45
|
export interface LynxShipConfig {
|
|
45
46
|
projectId?: string;
|
|
47
|
+
plugins?: PluginReference[];
|
|
46
48
|
cli?: {
|
|
47
49
|
apiUrl?: string;
|
|
48
50
|
organizationId?: string;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE;QACL,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChC,GAAG,CAAC,EAAE;QACJ,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;QAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;QAChC,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,GAAG,CAAC,EAAE;QACJ,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE;QACJ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,cAAc,CAAC,EAAE;QAAE,MAAM,EAAE,aAAa,GAAG,QAAQ,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACrC,MAAM,CAAC,EAAE;QACP,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE;YAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC1C,CAAC;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,cAAc,EAAE,cAsB5B,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/config.js
CHANGED
|
@@ -33,6 +33,7 @@ const allowedRootKeys = new Set([
|
|
|
33
33
|
"submit",
|
|
34
34
|
"update",
|
|
35
35
|
"artifacts",
|
|
36
|
+
"plugins",
|
|
36
37
|
]);
|
|
37
38
|
export function validateConfig(config, options = {}) {
|
|
38
39
|
assert(config && typeof config === "object" && !Array.isArray(config), "CONFIG_INVALID", "lynxship.json must contain an object");
|
|
@@ -41,6 +42,20 @@ export function validateConfig(config, options = {}) {
|
|
|
41
42
|
assert(!options.ci || unknown.length === 0, "CONFIG_UNKNOWN_KEY", `Unknown configuration key(s): ${unknown.join(", ")}`, { unknown });
|
|
42
43
|
if (value.projectId !== undefined)
|
|
43
44
|
assert(typeof value.projectId === "string" && value.projectId.length > 0, "CONFIG_PROJECT_ID", "projectId must be a non-empty string");
|
|
45
|
+
if (value.plugins !== undefined) {
|
|
46
|
+
assert(Array.isArray(value.plugins), "CONFIG_PLUGINS", "plugins must be an array of package names or [package, options] tuples");
|
|
47
|
+
for (const plugin of value.plugins) {
|
|
48
|
+
const validString = typeof plugin === "string" && plugin.length > 0;
|
|
49
|
+
const validTuple = Array.isArray(plugin) &&
|
|
50
|
+
plugin.length === 2 &&
|
|
51
|
+
typeof plugin[0] === "string" &&
|
|
52
|
+
plugin[0].length > 0 &&
|
|
53
|
+
plugin[1] !== null &&
|
|
54
|
+
typeof plugin[1] === "object" &&
|
|
55
|
+
!Array.isArray(plugin[1]);
|
|
56
|
+
assert(validString || validTuple, "CONFIG_PLUGINS", "Each plugin must be a package name or [package name, JSON options]");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
44
59
|
assert(value.runtimeVersion?.policy === undefined ||
|
|
45
60
|
["fingerprint", "manual"].includes(value.runtimeVersion.policy), "CONFIG_RUNTIME_POLICY", "runtimeVersion.policy must be fingerprint or manual");
|
|
46
61
|
if (value.runtimeVersion?.policy === "manual")
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,7 @@ import { hasHarmonyHost, runRealHarmonyBuild } from "./harmony-build.js";
|
|
|
35
35
|
import { hasWebConfiguration, runRealWebBuild } from "./web-build.js";
|
|
36
36
|
import { hasDesktopHost, runRealDesktopBuild } from "./desktop-build.js";
|
|
37
37
|
import { extractDevServerUrl, shouldPrintDevServerQr } from "./dev-qr.js";
|
|
38
|
+
import { applyProjectPlugins, inspectProjectPlugins } from "./plugins.js";
|
|
38
39
|
import { inspectDesktopTarget, inspectHarmonyTarget, inspectWebTarget, } from "./target-toolchain.js";
|
|
39
40
|
const rawArgs = process.argv.slice(2);
|
|
40
41
|
const args = [...rawArgs];
|
|
@@ -216,7 +217,20 @@ async function executeBuild(options, showResult = true) {
|
|
|
216
217
|
// Host initialization can write lynxship.json (for example, the generated
|
|
217
218
|
// iOS scheme). Always read it again before resolving the build profile so a
|
|
218
219
|
// first build uses the host that was just created in the same invocation.
|
|
219
|
-
const
|
|
220
|
+
const loadedConfig = wait && !local ? await loadConfig(root) : config;
|
|
221
|
+
const pluginApplication = wait && !local
|
|
222
|
+
? await applyProjectPlugins(root, loadedConfig, { platform, profile })
|
|
223
|
+
: {
|
|
224
|
+
config: loadedConfig,
|
|
225
|
+
report: { configured: 0, plugins: [] },
|
|
226
|
+
applied: [],
|
|
227
|
+
templates: [],
|
|
228
|
+
cloud: [],
|
|
229
|
+
build: [],
|
|
230
|
+
changes: [],
|
|
231
|
+
autolink: [],
|
|
232
|
+
};
|
|
233
|
+
const effectiveConfig = pluginApplication.config;
|
|
220
234
|
if (platform === "android" || platform === "ios")
|
|
221
235
|
await requireAutolinkReady(root, platform);
|
|
222
236
|
const runtime = await inspectRuntimeFingerprint(root, platform, effectiveConfig);
|
|
@@ -418,6 +432,7 @@ function commandTitle(command) {
|
|
|
418
432
|
devtool: "Lynx DevTool diagnostics",
|
|
419
433
|
trace: "Lynx Trace diagnostics",
|
|
420
434
|
recorder: "Lynx Recorder diagnostics",
|
|
435
|
+
plugin: "LynxShip plugins",
|
|
421
436
|
}[command] ?? command);
|
|
422
437
|
}
|
|
423
438
|
function helpText() {
|
|
@@ -433,6 +448,10 @@ Commands:
|
|
|
433
448
|
devtool doctor Check Lynx DevTool integration and dev runtime
|
|
434
449
|
trace doctor Check Lynx Trace prerequisites
|
|
435
450
|
recorder doctor Check Lynx Recorder prerequisites
|
|
451
|
+
plugin list List project plugins and their capabilities
|
|
452
|
+
plugin doctor Validate plugin packages without modifying native files
|
|
453
|
+
plugin apply Apply project plugin native changes for a platform
|
|
454
|
+
plugin apply --dry-run Preview plugin native changes without writing files
|
|
436
455
|
autolink check Check Lynx native-library Autolink wiring
|
|
437
456
|
autolink codegen Run the project's Native Module codegen script
|
|
438
457
|
ota doctor Check native OTA host integration
|
|
@@ -905,6 +924,80 @@ async function main() {
|
|
|
905
924
|
});
|
|
906
925
|
return;
|
|
907
926
|
}
|
|
927
|
+
if (command === "plugin") {
|
|
928
|
+
const subcommand = args.shift() ?? "list";
|
|
929
|
+
assert(["list", "doctor", "apply"].includes(subcommand), "CLI_PLUGIN_COMMAND", "Use \`lynxship plugin list\`, \`lynxship plugin doctor\` or \`lynxship plugin apply\`.");
|
|
930
|
+
await requireProjectRoot();
|
|
931
|
+
const config = await loadConfig(root);
|
|
932
|
+
const report = await inspectProjectPlugins(root, config);
|
|
933
|
+
if (subcommand === "apply") {
|
|
934
|
+
const platform = platformValue(flag("--platform", "android"));
|
|
935
|
+
const profile = flag("--profile", "production");
|
|
936
|
+
const dryRun = args.includes("--dry-run");
|
|
937
|
+
const result = await applyProjectPlugins(root, config, {
|
|
938
|
+
platform,
|
|
939
|
+
profile,
|
|
940
|
+
mode: dryRun ? "plan" : "apply",
|
|
941
|
+
});
|
|
942
|
+
printValue({
|
|
943
|
+
status: dryRun ? "planned" : "applied",
|
|
944
|
+
platform,
|
|
945
|
+
profile,
|
|
946
|
+
plugins: result.applied,
|
|
947
|
+
nativeChanges: result.changes.filter((change) => change.changed)
|
|
948
|
+
.length,
|
|
949
|
+
changes: result.changes,
|
|
950
|
+
templates: result.templates,
|
|
951
|
+
cloud: result.cloud,
|
|
952
|
+
build: result.build,
|
|
953
|
+
}, {
|
|
954
|
+
title: `LynxShip plugins · ${platform}`,
|
|
955
|
+
rows: [
|
|
956
|
+
{
|
|
957
|
+
label: "Applied",
|
|
958
|
+
value: result.applied.length ? result.applied.join(", ") : "none",
|
|
959
|
+
valueColor: "green",
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
label: "Native changes",
|
|
963
|
+
value: String(result.changes.filter((change) => change.changed).length),
|
|
964
|
+
valueColor: "blue",
|
|
965
|
+
},
|
|
966
|
+
],
|
|
967
|
+
done: dryRun
|
|
968
|
+
? "No native files were modified; review the planned changes."
|
|
969
|
+
: "Project plugin changes are applied atomically and idempotently.",
|
|
970
|
+
});
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
const invalid = report.plugins.filter((plugin) => plugin.status !== "ready");
|
|
974
|
+
printValue(report, {
|
|
975
|
+
title: `LynxShip plugins · ${subcommand}`,
|
|
976
|
+
rows: report.plugins.length > 0
|
|
977
|
+
? report.plugins.map((plugin) => ({
|
|
978
|
+
label: plugin.name,
|
|
979
|
+
value: `${plugin.status} · ${plugin.capabilities.join(", ") || "no capabilities"} · ${plugin.reason}`,
|
|
980
|
+
valueColor: plugin.status === "ready"
|
|
981
|
+
? "green"
|
|
982
|
+
: plugin.status === "missing"
|
|
983
|
+
? "red"
|
|
984
|
+
: "yellow",
|
|
985
|
+
}))
|
|
986
|
+
: [
|
|
987
|
+
{
|
|
988
|
+
label: "Plugins",
|
|
989
|
+
value: "none configured",
|
|
990
|
+
valueColor: "muted",
|
|
991
|
+
},
|
|
992
|
+
],
|
|
993
|
+
done: invalid.length === 0
|
|
994
|
+
? "All project plugin manifests are valid."
|
|
995
|
+
: "Fix the invalid plugin package before building.",
|
|
996
|
+
});
|
|
997
|
+
if (subcommand === "doctor" && invalid.length > 0)
|
|
998
|
+
process.exitCode = 1;
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
908
1001
|
if (command === "doctor") {
|
|
909
1002
|
const config = await loadConfig(root);
|
|
910
1003
|
const configuration = await readConfigurationStatus();
|
|
@@ -946,6 +1039,8 @@ async function main() {
|
|
|
946
1039
|
const nodeSupported = nodeMajor >= 22;
|
|
947
1040
|
const nodeRecommended = nodeMajor % 2 === 0;
|
|
948
1041
|
const framework = await detectLynxFramework(root);
|
|
1042
|
+
const pluginReport = await inspectProjectPlugins(root, config);
|
|
1043
|
+
const invalidPlugins = pluginReport.plugins.filter((plugin) => plugin.status !== "ready");
|
|
949
1044
|
const checks = [
|
|
950
1045
|
{
|
|
951
1046
|
name: "lynx-framework",
|
|
@@ -1002,6 +1097,16 @@ async function main() {
|
|
|
1002
1097
|
status: config.projectId ? "pass" : "fail",
|
|
1003
1098
|
value: config.projectId ? "found" : "missing · fix: lynxship init",
|
|
1004
1099
|
},
|
|
1100
|
+
{
|
|
1101
|
+
name: "lynxship-plugins",
|
|
1102
|
+
ok: invalidPlugins.length === 0,
|
|
1103
|
+
status: invalidPlugins.length === 0 ? "pass" : "fail",
|
|
1104
|
+
value: invalidPlugins.length === 0
|
|
1105
|
+
? pluginReport.configured === 0
|
|
1106
|
+
? "none configured"
|
|
1107
|
+
: `${pluginReport.configured} plugin(s) ready`
|
|
1108
|
+
: `${invalidPlugins.map((plugin) => plugin.name).join(", ")} · fix: lynxship plugin doctor`,
|
|
1109
|
+
},
|
|
1005
1110
|
{
|
|
1006
1111
|
name: "credential-store",
|
|
1007
1112
|
ok: true,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-api.d.ts","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@lynxship/plugin-api";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Platform } from "@lynxship/contracts";
|
|
2
|
+
import { type BuildContribution, type CloudIntegrationContribution, type PluginCapability, type AutolinkContribution, type PluginPermission, type PluginPlanChange, type TemplateContribution } from "@lynxship/plugin-api";
|
|
3
|
+
import type { LynxShipConfig } from "./config.js";
|
|
4
|
+
export interface ProjectPluginInfo {
|
|
5
|
+
name: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
packagePath?: string;
|
|
8
|
+
entry?: string;
|
|
9
|
+
apiVersion?: number;
|
|
10
|
+
capabilities: PluginCapability[];
|
|
11
|
+
permissions: PluginPermission[];
|
|
12
|
+
status: "ready" | "missing" | "invalid";
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
export interface PluginReport {
|
|
16
|
+
configured: number;
|
|
17
|
+
plugins: ProjectPluginInfo[];
|
|
18
|
+
}
|
|
19
|
+
export interface PluginApplication {
|
|
20
|
+
config: LynxShipConfig;
|
|
21
|
+
report: PluginReport;
|
|
22
|
+
applied: string[];
|
|
23
|
+
templates: TemplateContribution[];
|
|
24
|
+
cloud: CloudIntegrationContribution[];
|
|
25
|
+
build: BuildContribution[];
|
|
26
|
+
changes: PluginPlanChange[];
|
|
27
|
+
autolink: AutolinkContribution[];
|
|
28
|
+
}
|
|
29
|
+
export declare function inspectProjectPlugins(root: string, config: LynxShipConfig): Promise<PluginReport>;
|
|
30
|
+
export declare function applyProjectPlugins(root: string, config: LynxShipConfig, target: {
|
|
31
|
+
platform: Platform;
|
|
32
|
+
profile: string;
|
|
33
|
+
mode?: "plan" | "apply";
|
|
34
|
+
}): Promise<PluginApplication>;
|
|
35
|
+
//# sourceMappingURL=plugins.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugins.d.ts","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,4BAA4B,EAKjC,KAAK,gBAAgB,EAErB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EAGrB,KAAK,oBAAoB,EAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAclD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,gBAAgB,EAAE,CAAC;IACjC,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;IACxC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,iBAAiB,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,oBAAoB,EAAE,CAAC;IAClC,KAAK,EAAE,4BAA4B,EAAE,CAAC;IACtC,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,QAAQ,EAAE,oBAAoB,EAAE,CAAC;CAClC;AAqUD,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,YAAY,CAAC,CAQvB;AA+XD,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GACvE,OAAO,CAAC,iBAAiB,CAAC,CAqF5B"}
|
package/dist/plugins.js
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, rm, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { LYNXSHIP_PLUGIN_API_VERSION, } from "@lynxship/plugin-api";
|
|
7
|
+
function pluginReference(reference) {
|
|
8
|
+
if (typeof reference === "string")
|
|
9
|
+
return { name: reference, options: {} };
|
|
10
|
+
return { name: reference[0], options: reference[1] };
|
|
11
|
+
}
|
|
12
|
+
function packageDirectory(entry) {
|
|
13
|
+
let current = dirname(entry);
|
|
14
|
+
while (true) {
|
|
15
|
+
if (existsSync(join(current, "package.json")))
|
|
16
|
+
return current;
|
|
17
|
+
const parent = dirname(current);
|
|
18
|
+
if (parent === current)
|
|
19
|
+
return dirname(entry);
|
|
20
|
+
current = parent;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function parsePackageJson(file) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function packageJsonFromEntry(entry) {
|
|
32
|
+
return join(packageDirectory(entry), "package.json");
|
|
33
|
+
}
|
|
34
|
+
function findInstalledPackageJson(root, name) {
|
|
35
|
+
let current = root;
|
|
36
|
+
while (true) {
|
|
37
|
+
const candidate = join(current, "node_modules", ...name.split("/"), "package.json");
|
|
38
|
+
if (existsSync(candidate))
|
|
39
|
+
return candidate;
|
|
40
|
+
const parent = dirname(current);
|
|
41
|
+
if (parent === current)
|
|
42
|
+
return undefined;
|
|
43
|
+
current = parent;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function capabilities(value) {
|
|
47
|
+
if (!Array.isArray(value))
|
|
48
|
+
return [];
|
|
49
|
+
return value.filter((candidate) => ["config", "build", "native", "autolink", "template", "cloud"].includes(candidate));
|
|
50
|
+
}
|
|
51
|
+
const KNOWN_CAPABILITIES = [
|
|
52
|
+
"config",
|
|
53
|
+
"build",
|
|
54
|
+
"native",
|
|
55
|
+
"autolink",
|
|
56
|
+
"template",
|
|
57
|
+
"cloud",
|
|
58
|
+
];
|
|
59
|
+
const KNOWN_PERMISSIONS = [
|
|
60
|
+
"config:write",
|
|
61
|
+
"build:metadata",
|
|
62
|
+
"native:write",
|
|
63
|
+
"autolink:metadata",
|
|
64
|
+
"template:register",
|
|
65
|
+
"cloud:provider",
|
|
66
|
+
];
|
|
67
|
+
function isKnownList(value, allowed) {
|
|
68
|
+
return (Array.isArray(value) &&
|
|
69
|
+
value.every((candidate) => typeof candidate === "string" && allowed.includes(candidate)));
|
|
70
|
+
}
|
|
71
|
+
function permissions(value) {
|
|
72
|
+
if (!Array.isArray(value))
|
|
73
|
+
return [];
|
|
74
|
+
return value.filter((candidate) => [
|
|
75
|
+
"config:write",
|
|
76
|
+
"build:metadata",
|
|
77
|
+
"native:write",
|
|
78
|
+
"autolink:metadata",
|
|
79
|
+
"template:register",
|
|
80
|
+
"cloud:provider",
|
|
81
|
+
].includes(candidate));
|
|
82
|
+
}
|
|
83
|
+
function validPluginDefinition(value) {
|
|
84
|
+
if (!value || typeof value !== "object")
|
|
85
|
+
return false;
|
|
86
|
+
const definition = value;
|
|
87
|
+
return (definition.apiVersion === LYNXSHIP_PLUGIN_API_VERSION &&
|
|
88
|
+
typeof definition.name === "string" &&
|
|
89
|
+
definition.name.length > 0 &&
|
|
90
|
+
isKnownList(definition.capabilities, KNOWN_CAPABILITIES) &&
|
|
91
|
+
isKnownList(definition.permissions, KNOWN_PERMISSIONS) &&
|
|
92
|
+
(definition.platforms === undefined ||
|
|
93
|
+
isKnownList(definition.platforms, [
|
|
94
|
+
"all",
|
|
95
|
+
"android",
|
|
96
|
+
"ios",
|
|
97
|
+
"harmonyos",
|
|
98
|
+
"web",
|
|
99
|
+
"desktop",
|
|
100
|
+
])) &&
|
|
101
|
+
typeof definition.apply === "function");
|
|
102
|
+
}
|
|
103
|
+
function resolvePackageEntry(root, name) {
|
|
104
|
+
try {
|
|
105
|
+
const requireFromProject = createRequire(join(root, "package.json"));
|
|
106
|
+
let entry;
|
|
107
|
+
let packagePath;
|
|
108
|
+
try {
|
|
109
|
+
entry = requireFromProject.resolve(name);
|
|
110
|
+
packagePath = packageJsonFromEntry(entry);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
packagePath = findInstalledPackageJson(root, name) ?? "";
|
|
114
|
+
if (!packagePath)
|
|
115
|
+
throw new Error("Cannot find module '" + name + "'");
|
|
116
|
+
entry = packagePath;
|
|
117
|
+
}
|
|
118
|
+
const packageJson = parsePackageJson(packagePath);
|
|
119
|
+
const plugin = packageJson.lynxship?.plugin;
|
|
120
|
+
if (typeof plugin !== "string" || !plugin)
|
|
121
|
+
return {
|
|
122
|
+
packagePath,
|
|
123
|
+
reason: `${name} does not declare package.json#lynxship.plugin`,
|
|
124
|
+
};
|
|
125
|
+
const packageRoot = dirname(packagePath);
|
|
126
|
+
const resolved = resolve(packageRoot, plugin);
|
|
127
|
+
const outside = relative(packageRoot, resolved).startsWith("..");
|
|
128
|
+
if (outside)
|
|
129
|
+
return {
|
|
130
|
+
packagePath,
|
|
131
|
+
reason: `${name} points its plugin entry outside the package`,
|
|
132
|
+
};
|
|
133
|
+
if (!existsSync(resolved))
|
|
134
|
+
return {
|
|
135
|
+
packagePath,
|
|
136
|
+
reason: `${name} plugin entry does not exist: ${plugin}`,
|
|
137
|
+
};
|
|
138
|
+
return { packagePath, entry: resolved };
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
return {
|
|
142
|
+
reason: error instanceof Error
|
|
143
|
+
? `Cannot resolve ${name}: ${error.message}`
|
|
144
|
+
: `Cannot resolve ${name}`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function resolvePlugin(root, reference, load = true) {
|
|
149
|
+
const requested = pluginReference(reference);
|
|
150
|
+
const resolved = resolvePackageEntry(root, requested.name);
|
|
151
|
+
if (!resolved.packagePath || !resolved.entry) {
|
|
152
|
+
return {
|
|
153
|
+
info: {
|
|
154
|
+
name: requested.name,
|
|
155
|
+
capabilities: [],
|
|
156
|
+
permissions: [],
|
|
157
|
+
status: "missing",
|
|
158
|
+
reason: resolved.reason ?? "Plugin package was not found",
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const packageJson = parsePackageJson(resolved.packagePath);
|
|
163
|
+
const metadata = packageJson.lynxship;
|
|
164
|
+
const apiVersion = typeof metadata?.apiVersion === "number" ? metadata.apiVersion : undefined;
|
|
165
|
+
const packageCapabilities = capabilities(metadata?.capabilities);
|
|
166
|
+
const packagePermissions = permissions(metadata?.permissions);
|
|
167
|
+
if (!isKnownList(metadata?.capabilities, KNOWN_CAPABILITIES) ||
|
|
168
|
+
!isKnownList(metadata?.permissions, KNOWN_PERMISSIONS)) {
|
|
169
|
+
return {
|
|
170
|
+
info: {
|
|
171
|
+
name: requested.name,
|
|
172
|
+
version: typeof packageJson.version === "string"
|
|
173
|
+
? packageJson.version
|
|
174
|
+
: undefined,
|
|
175
|
+
packagePath: resolved.packagePath,
|
|
176
|
+
entry: resolved.entry,
|
|
177
|
+
apiVersion,
|
|
178
|
+
capabilities: packageCapabilities,
|
|
179
|
+
permissions: packagePermissions,
|
|
180
|
+
status: "invalid",
|
|
181
|
+
reason: "Plugin package capabilities and permissions must be arrays of known values",
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (apiVersion !== LYNXSHIP_PLUGIN_API_VERSION) {
|
|
186
|
+
return {
|
|
187
|
+
info: {
|
|
188
|
+
name: requested.name,
|
|
189
|
+
version: typeof packageJson.version === "string"
|
|
190
|
+
? packageJson.version
|
|
191
|
+
: undefined,
|
|
192
|
+
packagePath: resolved.packagePath,
|
|
193
|
+
entry: resolved.entry,
|
|
194
|
+
apiVersion,
|
|
195
|
+
capabilities: packageCapabilities,
|
|
196
|
+
permissions: packagePermissions,
|
|
197
|
+
status: "invalid",
|
|
198
|
+
reason: `Plugin API ${String(apiVersion ?? "missing")} is not supported; expected ${LYNXSHIP_PLUGIN_API_VERSION}`,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const info = {
|
|
203
|
+
name: requested.name,
|
|
204
|
+
version: typeof packageJson.version === "string" ? packageJson.version : undefined,
|
|
205
|
+
packagePath: resolved.packagePath,
|
|
206
|
+
entry: resolved.entry,
|
|
207
|
+
apiVersion,
|
|
208
|
+
capabilities: packageCapabilities,
|
|
209
|
+
permissions: packagePermissions,
|
|
210
|
+
status: "ready",
|
|
211
|
+
reason: "Plugin manifest is valid",
|
|
212
|
+
};
|
|
213
|
+
if (!load)
|
|
214
|
+
return { info };
|
|
215
|
+
try {
|
|
216
|
+
const module = (await import(pathToFileURL(resolved.entry).href));
|
|
217
|
+
const definition = module.default ?? module.plugin;
|
|
218
|
+
if (!validPluginDefinition(definition)) {
|
|
219
|
+
return {
|
|
220
|
+
info: {
|
|
221
|
+
...info,
|
|
222
|
+
status: "invalid",
|
|
223
|
+
reason: "Plugin entry must export a LynxShipPluginDefinition",
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (capabilities(definition.capabilities).join(",") !==
|
|
228
|
+
packageCapabilities.join(",") ||
|
|
229
|
+
permissions(definition.permissions).join(",") !==
|
|
230
|
+
packagePermissions.join(",")) {
|
|
231
|
+
return {
|
|
232
|
+
info: {
|
|
233
|
+
...info,
|
|
234
|
+
status: "invalid",
|
|
235
|
+
reason: "Plugin code capabilities/permissions do not match package.json",
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (definition.name !== requested.name &&
|
|
240
|
+
definition.name !== packageJson.name) {
|
|
241
|
+
return {
|
|
242
|
+
info: {
|
|
243
|
+
...info,
|
|
244
|
+
status: "invalid",
|
|
245
|
+
reason: `Plugin entry name '${definition.name}' does not match its package`,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return { info, definition };
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
return {
|
|
253
|
+
info: {
|
|
254
|
+
...info,
|
|
255
|
+
status: "invalid",
|
|
256
|
+
reason: error instanceof Error
|
|
257
|
+
? `Plugin entry failed to load: ${error.message}`
|
|
258
|
+
: "Plugin entry failed to load",
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function pluginReferences(config) {
|
|
264
|
+
const value = config.plugins;
|
|
265
|
+
if (!Array.isArray(value))
|
|
266
|
+
return [];
|
|
267
|
+
return value;
|
|
268
|
+
}
|
|
269
|
+
export async function inspectProjectPlugins(root, config) {
|
|
270
|
+
const references = pluginReferences(config);
|
|
271
|
+
const plugins = await Promise.all(references.map(async (reference) => (await resolvePlugin(root, reference, false)).info));
|
|
272
|
+
return { configured: references.length, plugins };
|
|
273
|
+
}
|
|
274
|
+
function appliesToPlatform(platforms, platform) {
|
|
275
|
+
return (!platforms ||
|
|
276
|
+
platforms.length === 0 ||
|
|
277
|
+
platforms.includes("all") ||
|
|
278
|
+
platforms.includes(platform));
|
|
279
|
+
}
|
|
280
|
+
function mergeJson(left, right) {
|
|
281
|
+
const result = { ...left };
|
|
282
|
+
for (const [key, value] of Object.entries(right)) {
|
|
283
|
+
const current = result[key];
|
|
284
|
+
if (current &&
|
|
285
|
+
typeof current === "object" &&
|
|
286
|
+
!Array.isArray(current) &&
|
|
287
|
+
value &&
|
|
288
|
+
typeof value === "object" &&
|
|
289
|
+
!Array.isArray(value)) {
|
|
290
|
+
result[key] = mergeJson(current, value);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
result[key] = value;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
function safeProjectFile(root, file) {
|
|
299
|
+
if (isAbsolute(file))
|
|
300
|
+
throw new Error(`Plugin native file must be relative: ${file}`);
|
|
301
|
+
const target = resolve(root, file);
|
|
302
|
+
const projectRelative = relative(root, target);
|
|
303
|
+
if (!projectRelative ||
|
|
304
|
+
projectRelative.startsWith("..") ||
|
|
305
|
+
isAbsolute(projectRelative))
|
|
306
|
+
throw new Error(`Plugin native file escapes the project: ${file}`);
|
|
307
|
+
return target;
|
|
308
|
+
}
|
|
309
|
+
function permissionForContribution(contribution) {
|
|
310
|
+
const required = [];
|
|
311
|
+
if (contribution.config)
|
|
312
|
+
required.push("config:write");
|
|
313
|
+
if (contribution.build)
|
|
314
|
+
required.push("build:metadata");
|
|
315
|
+
if (contribution.native?.length)
|
|
316
|
+
required.push("native:write");
|
|
317
|
+
if (contribution.autolink)
|
|
318
|
+
required.push("autolink:metadata");
|
|
319
|
+
if (contribution.templates?.length)
|
|
320
|
+
required.push("template:register");
|
|
321
|
+
if (contribution.cloud?.length)
|
|
322
|
+
required.push("cloud:provider");
|
|
323
|
+
return required;
|
|
324
|
+
}
|
|
325
|
+
function capabilityForPermission(permission) {
|
|
326
|
+
return permission.split(":")[0];
|
|
327
|
+
}
|
|
328
|
+
function isJsonObject(value) {
|
|
329
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
330
|
+
}
|
|
331
|
+
function assertPluginContribution(plugin, contribution) {
|
|
332
|
+
if (!isJsonObject(contribution))
|
|
333
|
+
throw new Error(`Plugin '${plugin.name}' returned an invalid contribution object`);
|
|
334
|
+
if (contribution.config !== undefined && !isJsonObject(contribution.config))
|
|
335
|
+
throw new Error(`Plugin '${plugin.name}' returned an invalid config contribution`);
|
|
336
|
+
if (contribution.native !== undefined && !Array.isArray(contribution.native))
|
|
337
|
+
throw new Error(`Plugin '${plugin.name}' returned an invalid native contribution`);
|
|
338
|
+
for (const operation of contribution.native ?? []) {
|
|
339
|
+
if (!isJsonObject(operation))
|
|
340
|
+
throw new Error(`Plugin '${plugin.name}' returned an invalid native operation`);
|
|
341
|
+
if (!["all", "android", "ios", "harmonyos", "web", "desktop"].includes(String(operation.platform)) ||
|
|
342
|
+
typeof operation.file !== "string" ||
|
|
343
|
+
operation.file.length === 0 ||
|
|
344
|
+
!["ensure-text", "append-text", "replace-text", "json-merge"].includes(String(operation.operation)))
|
|
345
|
+
throw new Error(`Plugin '${plugin.name}' returned an invalid native operation`);
|
|
346
|
+
if ((operation.operation === "ensure-text" ||
|
|
347
|
+
operation.operation === "append-text") &&
|
|
348
|
+
typeof operation.text !== "string")
|
|
349
|
+
throw new Error(`Plugin '${plugin.name}' returned an invalid text operation`);
|
|
350
|
+
if (operation.operation === "replace-text" &&
|
|
351
|
+
(typeof operation.from !== "string" || typeof operation.to !== "string"))
|
|
352
|
+
throw new Error(`Plugin '${plugin.name}' returned an invalid replacement operation`);
|
|
353
|
+
if (operation.operation === "json-merge" && !isJsonObject(operation.value))
|
|
354
|
+
throw new Error(`Plugin '${plugin.name}' returned an invalid JSON merge operation`);
|
|
355
|
+
}
|
|
356
|
+
if (contribution.autolink !== undefined) {
|
|
357
|
+
if (!isJsonObject(contribution.autolink))
|
|
358
|
+
throw new Error(`Plugin '${plugin.name}' returned an invalid Autolink contribution`);
|
|
359
|
+
for (const key of ["packageNames", "capabilities"]) {
|
|
360
|
+
const value = contribution.autolink[key];
|
|
361
|
+
if (value !== undefined &&
|
|
362
|
+
(!Array.isArray(value) ||
|
|
363
|
+
!value.every((item) => typeof item === "string")))
|
|
364
|
+
throw new Error(`Plugin '${plugin.name}' returned invalid Autolink metadata`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (contribution.build !== undefined) {
|
|
368
|
+
if (!isJsonObject(contribution.build))
|
|
369
|
+
throw new Error(`Plugin '${plugin.name}' returned an invalid build contribution`);
|
|
370
|
+
for (const key of [
|
|
371
|
+
"requiredTools",
|
|
372
|
+
"artifactExtensions",
|
|
373
|
+
"notes",
|
|
374
|
+
]) {
|
|
375
|
+
const value = contribution.build[key];
|
|
376
|
+
if (value !== undefined &&
|
|
377
|
+
(!Array.isArray(value) ||
|
|
378
|
+
!value.every((item) => typeof item === "string")))
|
|
379
|
+
throw new Error(`Plugin '${plugin.name}' returned invalid build metadata`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
for (const template of Array.isArray(contribution.templates)
|
|
383
|
+
? contribution.templates
|
|
384
|
+
: []) {
|
|
385
|
+
if (!isJsonObject(template) ||
|
|
386
|
+
typeof template.id !== "string" ||
|
|
387
|
+
typeof template.displayName !== "string" ||
|
|
388
|
+
typeof template.source !== "string")
|
|
389
|
+
throw new Error(`Plugin '${plugin.name}' returned an invalid template contribution`);
|
|
390
|
+
}
|
|
391
|
+
for (const integration of Array.isArray(contribution.cloud)
|
|
392
|
+
? contribution.cloud
|
|
393
|
+
: []) {
|
|
394
|
+
if (!isJsonObject(integration) ||
|
|
395
|
+
typeof integration.id !== "string" ||
|
|
396
|
+
typeof integration.displayName !== "string" ||
|
|
397
|
+
!Array.isArray(integration.capabilities) ||
|
|
398
|
+
!integration.capabilities.every((item) => ["build", "artifacts", "updates", "submit"].includes(String(item))))
|
|
399
|
+
throw new Error(`Plugin '${plugin.name}' returned an invalid cloud contribution`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function assertPluginPermissions(plugin, contribution) {
|
|
403
|
+
const missing = permissionForContribution(contribution).filter((permission) => !plugin.permissions.includes(permission));
|
|
404
|
+
const missingCapabilities = permissionForContribution(contribution)
|
|
405
|
+
.map(capabilityForPermission)
|
|
406
|
+
.filter((capability) => !plugin.capabilities.includes(capability));
|
|
407
|
+
if (missing.length)
|
|
408
|
+
throw new Error(`Plugin '${plugin.name}' returned contributions without declaring permission(s): ${missing.join(", ")}`);
|
|
409
|
+
if (missingCapabilities.length)
|
|
410
|
+
throw new Error(`Plugin '${plugin.name}' returned contributions without declaring capability(ies): ${[...new Set(missingCapabilities)].join(", ")}`);
|
|
411
|
+
}
|
|
412
|
+
async function mergeJsonFile(file, value) {
|
|
413
|
+
const current = JSON.parse(await readFile(file, "utf8"));
|
|
414
|
+
await writeFile(file, `${JSON.stringify(mergeJson(current, value), null, 2)}\n`);
|
|
415
|
+
}
|
|
416
|
+
async function inspectNativeOperation(root, owned, platform) {
|
|
417
|
+
const operation = owned.operation;
|
|
418
|
+
if (!appliesToPlatform([operation.platform], platform))
|
|
419
|
+
return undefined;
|
|
420
|
+
const file = safeProjectFile(root, operation.file);
|
|
421
|
+
const current = existsSync(file) ? await readFile(file, "utf8") : "";
|
|
422
|
+
let changed = false;
|
|
423
|
+
if (operation.operation === "json-merge") {
|
|
424
|
+
if (!existsSync(file))
|
|
425
|
+
throw new Error(`Plugin JSON target does not exist: ${operation.file}`);
|
|
426
|
+
const merged = `${JSON.stringify(mergeJson(JSON.parse(current), operation.value), null, 2)}\n`;
|
|
427
|
+
changed = merged !== current;
|
|
428
|
+
}
|
|
429
|
+
else if (operation.operation === "ensure-text") {
|
|
430
|
+
changed = !current.includes(operation.text);
|
|
431
|
+
}
|
|
432
|
+
else if (operation.operation === "append-text") {
|
|
433
|
+
changed = !current.includes(operation.text);
|
|
434
|
+
}
|
|
435
|
+
else if (operation.operation === "replace-text") {
|
|
436
|
+
if (!current.includes(operation.from))
|
|
437
|
+
throw new Error(`Plugin replacement target was not found in ${operation.file}`);
|
|
438
|
+
changed = current.replace(operation.from, operation.to) !== current;
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
plugin: owned.plugin,
|
|
442
|
+
platform,
|
|
443
|
+
file: operation.file,
|
|
444
|
+
operation: operation.operation,
|
|
445
|
+
changed,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
async function applyNativeOperation(root, operation, platform) {
|
|
449
|
+
if (!appliesToPlatform([operation.platform], platform))
|
|
450
|
+
return;
|
|
451
|
+
const file = safeProjectFile(root, operation.file);
|
|
452
|
+
await mkdir(dirname(file), { recursive: true });
|
|
453
|
+
if (operation.operation === "json-merge") {
|
|
454
|
+
if (!existsSync(file))
|
|
455
|
+
throw new Error(`Plugin JSON target does not exist: ${operation.file}`);
|
|
456
|
+
await mergeJsonFile(file, operation.value);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const current = existsSync(file) ? await readFile(file, "utf8") : "";
|
|
460
|
+
if (operation.operation === "ensure-text") {
|
|
461
|
+
if (!current.includes(operation.text))
|
|
462
|
+
await writeFile(file, `${current}${current && !current.endsWith("\n") ? "\n" : ""}${operation.text}\n`);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (operation.operation === "append-text") {
|
|
466
|
+
if (current.includes(operation.text))
|
|
467
|
+
return;
|
|
468
|
+
await writeFile(file, `${current}${current && !current.endsWith("\n") ? "\n" : ""}${operation.text}\n`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (operation.operation !== "replace-text")
|
|
472
|
+
return;
|
|
473
|
+
if (!current.includes(operation.from))
|
|
474
|
+
throw new Error(`Plugin replacement target was not found in ${operation.file}`);
|
|
475
|
+
await writeFile(file, current.replace(operation.from, operation.to));
|
|
476
|
+
}
|
|
477
|
+
function assertNativeConflicts(root, operations) {
|
|
478
|
+
const seen = new Map();
|
|
479
|
+
for (const owned of operations) {
|
|
480
|
+
const file = safeProjectFile(root, owned.operation.file);
|
|
481
|
+
const previous = seen.get(file);
|
|
482
|
+
if (!previous) {
|
|
483
|
+
seen.set(file, owned);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const currentOperation = owned.operation;
|
|
487
|
+
const previousOperation = previous.operation;
|
|
488
|
+
if (currentOperation.operation === "replace-text" &&
|
|
489
|
+
previousOperation.operation === "replace-text" &&
|
|
490
|
+
currentOperation.from === previousOperation.from)
|
|
491
|
+
throw new Error(`Conflicting or duplicate LynxShip plugins '${previous.plugin}' and '${owned.plugin}' both replace '${currentOperation.from}' in ${currentOperation.file}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async function applyNativeOperationsAtomically(root, operations, platform) {
|
|
495
|
+
assertNativeConflicts(root, operations);
|
|
496
|
+
const backups = new Map();
|
|
497
|
+
for (const owned of operations) {
|
|
498
|
+
if (!appliesToPlatform([owned.operation.platform], platform))
|
|
499
|
+
continue;
|
|
500
|
+
const file = safeProjectFile(root, owned.operation.file);
|
|
501
|
+
if (!backups.has(file))
|
|
502
|
+
backups.set(file, existsSync(file) ? await readFile(file, "utf8") : undefined);
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
const changes = (await Promise.all(operations.map((operation) => inspectNativeOperation(root, operation, platform)))).filter((change) => Boolean(change));
|
|
506
|
+
for (const owned of operations)
|
|
507
|
+
await applyNativeOperation(root, owned.operation, platform);
|
|
508
|
+
return changes;
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
for (const [file, content] of backups) {
|
|
512
|
+
if (content === undefined)
|
|
513
|
+
await rm(file, { force: true });
|
|
514
|
+
else {
|
|
515
|
+
await mkdir(dirname(file), { recursive: true });
|
|
516
|
+
await writeFile(file, content);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}; native plugin changes were rolled back`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
export async function applyProjectPlugins(root, config, target) {
|
|
523
|
+
const references = pluginReferences(config);
|
|
524
|
+
const resolved = [];
|
|
525
|
+
for (const reference of references)
|
|
526
|
+
resolved.push(await resolvePlugin(root, reference));
|
|
527
|
+
const report = {
|
|
528
|
+
configured: references.length,
|
|
529
|
+
plugins: resolved.map((plugin) => plugin.info),
|
|
530
|
+
};
|
|
531
|
+
const invalid = report.plugins.find((plugin) => plugin.status !== "ready");
|
|
532
|
+
if (invalid)
|
|
533
|
+
throw new Error(`LynxShip plugin '${invalid.name}' is invalid: ${invalid.reason}`);
|
|
534
|
+
let effectiveConfig = JSON.parse(JSON.stringify(config));
|
|
535
|
+
const applied = [];
|
|
536
|
+
const templates = [];
|
|
537
|
+
const cloud = [];
|
|
538
|
+
const build = [];
|
|
539
|
+
const autolink = [];
|
|
540
|
+
const operations = [];
|
|
541
|
+
for (let index = 0; index < resolved.length; index += 1) {
|
|
542
|
+
const plugin = resolved[index];
|
|
543
|
+
const reference = references[index];
|
|
544
|
+
if (!plugin?.definition || !reference)
|
|
545
|
+
continue;
|
|
546
|
+
if (!appliesToPlatform(plugin.definition.platforms, target.platform))
|
|
547
|
+
continue;
|
|
548
|
+
const options = pluginReference(reference).options;
|
|
549
|
+
const context = {
|
|
550
|
+
rootDir: root,
|
|
551
|
+
platform: target.platform,
|
|
552
|
+
profile: target.profile,
|
|
553
|
+
mode: target.mode ?? "apply",
|
|
554
|
+
projectId: config.projectId,
|
|
555
|
+
config: effectiveConfig,
|
|
556
|
+
options,
|
|
557
|
+
};
|
|
558
|
+
const contribution = await plugin.definition.apply(context);
|
|
559
|
+
if (!contribution) {
|
|
560
|
+
applied.push(plugin.info.name);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
assertPluginContribution(plugin.definition, contribution);
|
|
564
|
+
assertPluginPermissions(plugin.definition, contribution);
|
|
565
|
+
if (contribution.config)
|
|
566
|
+
effectiveConfig = mergeJson(effectiveConfig, contribution.config);
|
|
567
|
+
for (const operation of contribution.native ?? [])
|
|
568
|
+
operations.push({ plugin: plugin.info.name, operation });
|
|
569
|
+
if (contribution.autolink)
|
|
570
|
+
autolink.push(contribution.autolink);
|
|
571
|
+
templates.push(...(contribution.templates ?? []));
|
|
572
|
+
cloud.push(...(contribution.cloud ?? []));
|
|
573
|
+
if (contribution.build)
|
|
574
|
+
build.push(contribution.build);
|
|
575
|
+
applied.push(plugin.info.name);
|
|
576
|
+
}
|
|
577
|
+
let changes;
|
|
578
|
+
if (target.mode === "plan") {
|
|
579
|
+
assertNativeConflicts(root, operations);
|
|
580
|
+
changes = (await Promise.all(operations.map((operation) => inspectNativeOperation(root, operation, target.platform)))).filter((change) => Boolean(change));
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
changes = await applyNativeOperationsAtomically(root, operations, target.platform);
|
|
584
|
+
}
|
|
585
|
+
return {
|
|
586
|
+
config: effectiveConfig,
|
|
587
|
+
report,
|
|
588
|
+
applied,
|
|
589
|
+
templates,
|
|
590
|
+
cloud,
|
|
591
|
+
build,
|
|
592
|
+
autolink,
|
|
593
|
+
changes,
|
|
594
|
+
};
|
|
595
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lynxship/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Build, sign, store, submit and update LynxJS applications from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -58,6 +58,10 @@
|
|
|
58
58
|
"types": "./dist/frameworks.d.ts",
|
|
59
59
|
"import": "./dist/frameworks.js"
|
|
60
60
|
},
|
|
61
|
+
"./plugin-api": {
|
|
62
|
+
"types": "./dist/plugin-api.d.ts",
|
|
63
|
+
"import": "./dist/plugin-api.js"
|
|
64
|
+
},
|
|
61
65
|
"./ui/*": {
|
|
62
66
|
"types": "./dist/ui/*.d.ts",
|
|
63
67
|
"import": "./dist/ui/*.js"
|
|
@@ -76,9 +80,10 @@
|
|
|
76
80
|
"@resvg/resvg-js": "2.6.2",
|
|
77
81
|
"jsdom": "29.1.1",
|
|
78
82
|
"pngjs": "7.0.0",
|
|
79
|
-
"@lynxship/contracts": "0.1.0",
|
|
80
83
|
"@lynxship/build-orchestrator": "0.1.0",
|
|
84
|
+
"@lynxship/contracts": "0.1.0",
|
|
81
85
|
"@lynxship/submit": "0.1.0",
|
|
86
|
+
"@lynxship/plugin-api": "0.1.0",
|
|
82
87
|
"@lynxship/signing": "0.1.0",
|
|
83
88
|
"@lynxship/db": "0.1.0"
|
|
84
89
|
},
|