Skip to content

Commit 87cbca9

Browse files
authored
feat: hooks command (#5918)
1 parent f81499d commit 87cbca9

File tree

13 files changed

+547
-185
lines changed

13 files changed

+547
-185
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<% if (isJekyll) { %>---
2+
title: ns hooks
3+
position: 1
4+
---<% } %>
5+
6+
# ns create
7+
8+
### Description
9+
10+
Manages lifecycle hooks from installed plugins.
11+
12+
### Commands
13+
14+
Usage | Synopsis
15+
---------|---------
16+
Install | `$ ns hooks install`
17+
List | `$ ns hooks list`
18+
Lock | `$ ns hooks lock`
19+
Verify | `$ ns hooks verify`
20+
21+
#### Install
22+
23+
Installs hooks from each installed plugin dependency.
24+
25+
#### List
26+
27+
Lists the plugins which have hooks and which scripts they install
28+
29+
#### Lock
30+
31+
Generates a `hooks-lock.json` containing the hooks that are in the current versions of the plugins.
32+
33+
#### Verify
34+
35+
Verifies that the hooks contained in the installed plugins match those listed in the `hooks-lock.json` file.

docs/man_pages/start.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Command | Description
5151
[plugin](lib-management/plugin.html) | Lets you manage the plugins for your project.
5252
[open](project/configuration/open.md) | Opens the native project in Xcode/Android Studio.
5353
[widget ios](project/configuration/widget.md) | Adds a new iOS widget to the project.
54+
[hooks](project/hooks/hooks.html) | Installs lifecycle hooks from plugins.
5455
## Publishing Commands
5556
Command | Description
5657
---|---

lib/bootstrap.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,15 @@ injector.requireCommand("plugin|update", "./commands/plugin/update-plugin");
263263
injector.requireCommand("plugin|build", "./commands/plugin/build-plugin");
264264
injector.requireCommand("plugin|create", "./commands/plugin/create-plugin");
265265

266+
injector.requireCommand(
267+
["hooks|*list", "hooks|install"],
268+
"./commands/hooks/hooks",
269+
);
270+
injector.requireCommand(
271+
["hooks|lock", "hooks|verify"],
272+
"./commands/hooks/hooks-lock",
273+
);
274+
266275
injector.require("doctorService", "./services/doctor-service");
267276
injector.require("xcprojService", "./services/xcproj-service");
268277
injector.require("versionsService", "./services/versions-service");

lib/commands/hooks/common.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import * as _ from "lodash";
2+
import { IProjectData } from "../../definitions/project";
3+
import { IPluginData } from "../../definitions/plugins";
4+
import { ICommandParameter } from "../../common/definitions/commands";
5+
import { IErrors, IFileSystem } from "../../common/declarations";
6+
import path = require("path");
7+
import * as crypto from "crypto";
8+
9+
export const LOCK_FILE_NAME = "nativescript-lock.json";
10+
export interface OutputHook {
11+
type: string;
12+
hash: string;
13+
}
14+
15+
export interface OutputPlugin {
16+
name: string;
17+
hooks: OutputHook[];
18+
}
19+
20+
export class HooksVerify {
21+
public allowedParameters: ICommandParameter[] = [];
22+
23+
constructor(
24+
protected $projectData: IProjectData,
25+
protected $errors: IErrors,
26+
protected $fs: IFileSystem,
27+
protected $logger: ILogger,
28+
) {
29+
this.$projectData.initializeProjectData();
30+
}
31+
32+
protected async verifyHooksLock(
33+
plugins: IPluginData[],
34+
hooksLockPath: string,
35+
): Promise<void> {
36+
let lockFileContent: string;
37+
let hooksLock: OutputPlugin[];
38+
39+
try {
40+
lockFileContent = this.$fs.readText(hooksLockPath, "utf8");
41+
hooksLock = JSON.parse(lockFileContent);
42+
} catch (err) {
43+
this.$errors.fail(
44+
`❌ Failed to read or parse ${LOCK_FILE_NAME} at ${hooksLockPath}`,
45+
);
46+
}
47+
48+
const lockMap = new Map<string, Map<string, string>>(); // pluginName -> hookType -> hash
49+
50+
for (const plugin of hooksLock) {
51+
const hookMap = new Map<string, string>();
52+
for (const hook of plugin.hooks) {
53+
hookMap.set(hook.type, hook.hash);
54+
}
55+
lockMap.set(plugin.name, hookMap);
56+
}
57+
58+
let isValid = true;
59+
60+
for (const plugin of plugins) {
61+
const pluginLockHooks = lockMap.get(plugin.name);
62+
63+
if (!pluginLockHooks) {
64+
this.$logger.error(
65+
`❌ Plugin '${plugin.name}' not found in ${LOCK_FILE_NAME}`,
66+
);
67+
isValid = false;
68+
continue;
69+
}
70+
71+
for (const hook of plugin.nativescript?.hooks || []) {
72+
const expectedHash = pluginLockHooks.get(hook.type);
73+
74+
if (!expectedHash) {
75+
this.$logger.error(
76+
`❌ Missing hook '${hook.type}' for plugin '${plugin.name}' in ${LOCK_FILE_NAME}`,
77+
);
78+
isValid = false;
79+
continue;
80+
}
81+
82+
let fileContent: string | Buffer<ArrayBufferLike>;
83+
84+
try {
85+
fileContent = this.$fs.readFile(
86+
path.join(plugin.fullPath, hook.script),
87+
);
88+
} catch (err) {
89+
this.$logger.error(
90+
`❌ Cannot read script file '${hook.script}' for hook '${hook.type}' in plugin '${plugin.name}'`,
91+
);
92+
isValid = false;
93+
continue;
94+
}
95+
96+
const actualHash = crypto
97+
.createHash("sha256")
98+
.update(fileContent)
99+
.digest("hex");
100+
101+
if (actualHash !== expectedHash) {
102+
this.$logger.error(
103+
`❌ Hash mismatch for '${hook.script}' (${hook.type} in ${plugin.name}):`,
104+
);
105+
this.$logger.error(` Expected: ${expectedHash}`);
106+
this.$logger.error(` Actual: ${actualHash}`);
107+
isValid = false;
108+
}
109+
}
110+
}
111+
112+
if (isValid) {
113+
this.$logger.info("✅ All hooks verified successfully. No issues found.");
114+
} else {
115+
this.$errors.fail("❌ One or more hooks failed verification.");
116+
}
117+
}
118+
}

lib/commands/hooks/hooks-lock.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { IProjectData } from "../../definitions/project";
2+
import { IPluginsService, IPluginData } from "../../definitions/plugins";
3+
import { ICommand, ICommandParameter } from "../../common/definitions/commands";
4+
import { IErrors, IFileSystem } from "../../common/declarations";
5+
import { injector } from "../../common/yok";
6+
import path = require("path");
7+
import * as crypto from "crypto";
8+
import {
9+
HooksVerify,
10+
LOCK_FILE_NAME,
11+
OutputHook,
12+
OutputPlugin,
13+
} from "./common";
14+
15+
export class HooksLockPluginCommand implements ICommand {
16+
public allowedParameters: ICommandParameter[] = [];
17+
18+
constructor(
19+
private $pluginsService: IPluginsService,
20+
private $projectData: IProjectData,
21+
private $errors: IErrors,
22+
private $fs: IFileSystem,
23+
private $logger: ILogger,
24+
) {
25+
this.$projectData.initializeProjectData();
26+
}
27+
28+
public async execute(): Promise<void> {
29+
const plugins: IPluginData[] =
30+
await this.$pluginsService.getAllInstalledPlugins(this.$projectData);
31+
if (plugins && plugins.length > 0) {
32+
const pluginsWithHooks: IPluginData[] = [];
33+
for (const plugin of plugins) {
34+
if (plugin.nativescript?.hooks?.length > 0) {
35+
pluginsWithHooks.push(plugin);
36+
}
37+
}
38+
39+
await this.writeHooksLockFile(
40+
pluginsWithHooks,
41+
this.$projectData.projectDir,
42+
);
43+
} else {
44+
this.$logger.info("No plugins with hooks found.");
45+
}
46+
}
47+
48+
public async canExecute(args: string[]): Promise<boolean> {
49+
return true;
50+
}
51+
52+
private async writeHooksLockFile(
53+
plugins: IPluginData[],
54+
outputDir: string,
55+
): Promise<void> {
56+
const output: OutputPlugin[] = [];
57+
58+
for (const plugin of plugins) {
59+
const hooks: OutputHook[] = [];
60+
61+
for (const hook of plugin.nativescript?.hooks || []) {
62+
try {
63+
const fileContent = this.$fs.readFile(
64+
path.join(plugin.fullPath, hook.script),
65+
);
66+
const hash = crypto
67+
.createHash("sha256")
68+
.update(fileContent)
69+
.digest("hex");
70+
71+
hooks.push({
72+
type: hook.type,
73+
hash,
74+
});
75+
} catch (err) {
76+
this.$logger.warn(
77+
`Warning: Failed to read script '${hook.script}' for plugin '${plugin.name}'. Skipping this hook.`,
78+
);
79+
continue;
80+
}
81+
}
82+
83+
output.push({ name: plugin.name, hooks });
84+
}
85+
86+
const filePath = path.resolve(outputDir, LOCK_FILE_NAME);
87+
88+
try {
89+
this.$fs.writeFile(filePath, JSON.stringify(output, null, 2), "utf8");
90+
this.$logger.info(`✅ ${LOCK_FILE_NAME} written to: ${filePath}`);
91+
} catch (err) {
92+
this.$errors.fail(`❌ Failed to write ${LOCK_FILE_NAME}: ${err}`);
93+
}
94+
}
95+
}
96+
97+
export class HooksVerifyPluginCommand extends HooksVerify implements ICommand {
98+
public allowedParameters: ICommandParameter[] = [];
99+
100+
constructor(
101+
private $pluginsService: IPluginsService,
102+
$projectData: IProjectData,
103+
$errors: IErrors,
104+
$fs: IFileSystem,
105+
$logger: ILogger,
106+
) {
107+
super($projectData, $errors, $fs, $logger);
108+
}
109+
110+
public async execute(): Promise<void> {
111+
const plugins: IPluginData[] =
112+
await this.$pluginsService.getAllInstalledPlugins(this.$projectData);
113+
if (plugins && plugins.length > 0) {
114+
const pluginsWithHooks: IPluginData[] = [];
115+
for (const plugin of plugins) {
116+
if (plugin.nativescript?.hooks?.length > 0) {
117+
pluginsWithHooks.push(plugin);
118+
}
119+
}
120+
await this.verifyHooksLock(
121+
pluginsWithHooks,
122+
path.join(this.$projectData.projectDir, LOCK_FILE_NAME),
123+
);
124+
} else {
125+
this.$logger.info("No plugins with hooks found.");
126+
}
127+
}
128+
129+
public async canExecute(args: string[]): Promise<boolean> {
130+
return true;
131+
}
132+
}
133+
134+
injector.registerCommand(["hooks|lock"], HooksLockPluginCommand);
135+
injector.registerCommand(["hooks|verify"], HooksVerifyPluginCommand);

0 commit comments

Comments
 (0)