@gorilla-engine-sdk/gorilla-engine-scripts 1.3.4

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/lib/assets.js ADDED
@@ -0,0 +1,188 @@
1
+ import { copyFile, mkdir, readdir, rm } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import {
5
+ assertNoSymlinksWithinBase,
6
+ resolveExistingWithinBase,
7
+ resolveWithinBase,
8
+ ts,
9
+ } from './utils.js';
10
+ import { getDeployFunction } from './crypto.js';
11
+
12
+ /**
13
+ * Recursively syncs a source directory to a destination, removing entries that
14
+ * no longer exist in the source.
15
+ */
16
+ export async function syncAssetPath(sourcePath, destPath, destinationRoot = destPath) {
17
+ try {
18
+ const sourceEntries = await readdir(sourcePath, { withFileTypes: true });
19
+ await assertNoSymlinksWithinBase(destinationRoot, destPath);
20
+ await mkdir(destPath, { recursive: true });
21
+ await assertNoSymlinksWithinBase(destinationRoot, destPath);
22
+
23
+ const sourceNames = new Set();
24
+ for (const sourceEntry of sourceEntries) {
25
+ if (sourceEntry.isSymbolicLink()) {
26
+ throw new Error(`Refusing to deploy symbolic link "${path.join(sourcePath, sourceEntry.name)}"`);
27
+ }
28
+ sourceNames.add(sourceEntry.name);
29
+ const childSourcePath = path.join(sourcePath, sourceEntry.name);
30
+ const childDestPath = path.join(destPath, sourceEntry.name);
31
+ if (sourceEntry.isDirectory()) {
32
+ await syncAssetPath(childSourcePath, childDestPath, destinationRoot);
33
+ } else {
34
+ await assertNoSymlinksWithinBase(destinationRoot, childDestPath);
35
+ await mkdir(path.dirname(childDestPath), { recursive: true });
36
+ await copyFile(childSourcePath, childDestPath);
37
+ }
38
+ }
39
+
40
+ const destEntries = await readdir(destPath, { withFileTypes: true });
41
+ for (const destEntry of destEntries) {
42
+ if (!sourceNames.has(destEntry.name)) {
43
+ const staleDestPath = path.join(destPath, destEntry.name);
44
+ await assertNoSymlinksWithinBase(destinationRoot, staleDestPath);
45
+ await rm(staleDestPath, { recursive: true, force: true });
46
+ }
47
+ }
48
+ } catch (error) {
49
+ if (error.code !== 'ENOTDIR') throw error;
50
+ // sourcePath is a file but destPath is a directory (or vice versa) — replace it
51
+ await assertNoSymlinksWithinBase(destinationRoot, destPath);
52
+ await rm(destPath, { recursive: true, force: true });
53
+ await mkdir(path.dirname(destPath), { recursive: true });
54
+ await copyFile(sourcePath, destPath);
55
+ }
56
+ }
57
+
58
+ function resolvePresetDestPath(outputFolder, presetPath) {
59
+ const normalizedPresetPath = presetPath.replaceAll('\\', '/');
60
+ const pathParts = normalizedPresetPath.split('/').filter(Boolean);
61
+ const presetsPartIndex = pathParts.findIndex((part) => part.toLowerCase() === 'presets');
62
+
63
+ if (presetsPartIndex >= 0) {
64
+ const relativePresetPath = pathParts.slice(presetsPartIndex + 1);
65
+ return resolveWithinBase(outputFolder, 'Presets', ...relativePresetPath);
66
+ }
67
+
68
+ return resolveWithinBase(outputFolder, 'Presets', path.basename(normalizedPresetPath));
69
+ }
70
+
71
+ async function deployContentFiles(ugepFolder, destinationRoot, entries, resolveDestPath) {
72
+ for (const { path: sourcePath } of entries) {
73
+ const unresolvedSourcePath = resolveWithinBase(ugepFolder, sourcePath);
74
+ const contentDestPath = resolveDestPath(sourcePath);
75
+ if (!existsSync(unresolvedSourcePath)) {
76
+ await assertNoSymlinksWithinBase(destinationRoot, contentDestPath);
77
+ await rm(contentDestPath, { recursive: true, force: true });
78
+ continue;
79
+ }
80
+ const contentSourcePath = await resolveExistingWithinBase(ugepFolder, sourcePath);
81
+ await syncAssetPath(contentSourcePath, contentDestPath, destinationRoot);
82
+ }
83
+ }
84
+
85
+ export function deployAssetFilesToOutput({ ugepContent, ugepFolder, outputRoot, outputFolder }) {
86
+ return deployContentFiles(
87
+ ugepFolder,
88
+ outputRoot ?? outputFolder,
89
+ ugepContent.assetFiles ?? [],
90
+ (assetPath) => {
91
+ const assetDestPath = resolveWithinBase(outputFolder, assetPath);
92
+ if (assetDestPath === path.resolve(outputFolder)) {
93
+ throw new Error('Refusing to synchronize assets to the plugin output root');
94
+ }
95
+ return assetDestPath;
96
+ },
97
+ );
98
+ }
99
+
100
+ export function deployPresetFilesToOutput({ ugepContent, ugepFolder, outputRoot, outputFolder }) {
101
+ return deployContentFiles(
102
+ ugepFolder,
103
+ outputRoot ?? outputFolder,
104
+ ugepContent.presets ?? [],
105
+ (presetPath) => resolvePresetDestPath(outputFolder, presetPath),
106
+ );
107
+ }
108
+
109
+ /** Encrypts/copies the built JS + YAML + asset files to the plugin output folder. */
110
+ export async function deployAll(config) {
111
+ const { ugepContent, ugepFolder, outputRoot, outputFolder, pluginName } = config;
112
+ const deployFn = await getDeployFunction(config);
113
+ const promises = [];
114
+
115
+ if (ugepContent.ymlFiles?.length > 0) {
116
+ const yamlSourcePath = await resolveExistingWithinBase(
117
+ ugepFolder,
118
+ ugepContent.ymlFiles[0].path,
119
+ );
120
+ const yamlDestPath = resolveWithinBase(outputFolder, `${pluginName}.yaml`);
121
+ await assertNoSymlinksWithinBase(outputRoot ?? outputFolder, yamlDestPath);
122
+ promises.push(deployFn(yamlSourcePath, yamlDestPath));
123
+ }
124
+
125
+ const scriptDestPath = resolveWithinBase(outputFolder, `${pluginName}.js`);
126
+ await assertNoSymlinksWithinBase(outputRoot ?? outputFolder, scriptDestPath);
127
+ promises.push(deployFn(`./esbuild/${pluginName}.js`, scriptDestPath));
128
+ promises.push(deployAssetFilesToOutput(config));
129
+ promises.push(deployPresetFilesToOutput(config));
130
+
131
+ await Promise.all(promises);
132
+ }
133
+
134
+ /**
135
+ * Manages debounced asset deployment triggered by file-system watch events.
136
+ * Exposes a `suppressWatcher` flag used by InstrumentExporter to avoid
137
+ * double-deploying when it writes new blobs.
138
+ */
139
+ export class AssetDeployer {
140
+ suppressWatcher = false;
141
+
142
+ #config;
143
+ #standaloneManager;
144
+ #timer = null;
145
+ #inProgress = false;
146
+ #rerun = false;
147
+
148
+ constructor(config, standaloneManager) {
149
+ this.#config = config;
150
+ this.#standaloneManager = standaloneManager;
151
+ }
152
+
153
+ schedule() {
154
+ clearTimeout(this.#timer);
155
+ this.#timer = setTimeout(() => {
156
+ this.#timer = null;
157
+ void this.#run();
158
+ }, 250);
159
+ }
160
+
161
+ clearTimer() {
162
+ clearTimeout(this.#timer);
163
+ this.#timer = null;
164
+ }
165
+
166
+ async #run() {
167
+ if (this.#inProgress) {
168
+ this.#rerun = true;
169
+ return;
170
+ }
171
+ this.#inProgress = true;
172
+ try {
173
+ console.log(`[${ts()}] Asset changes detected - deploying`);
174
+ await deployAssetFilesToOutput(this.#config);
175
+ await deployPresetFilesToOutput(this.#config);
176
+ console.log(`[${ts()}] Asset deploy finished\n`);
177
+ if (this.#config.flags.standalone) await this.#standaloneManager.restart();
178
+ } catch (error) {
179
+ console.error(`[${ts()}] Asset deploy failed: ${error.message}`);
180
+ } finally {
181
+ this.#inProgress = false;
182
+ if (this.#rerun) {
183
+ this.#rerun = false;
184
+ void this.#run();
185
+ }
186
+ }
187
+ }
188
+ }
package/lib/build.js ADDED
@@ -0,0 +1,77 @@
1
+ import * as esbuild from 'esbuild';
2
+ import esbuildPluginTsc from 'esbuild-plugin-tsc';
3
+ import * as path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ export const externalizeNativeAddonsPlugin = {
7
+ name: 'externalize-native-addons',
8
+ setup(build) {
9
+ build.onResolve({ filter: /\.node$/ }, (args) => ({
10
+ path: args.path,
11
+ external: true,
12
+ }));
13
+
14
+ build.onResolve({ filter: /^[^./]/ }, async (args) => {
15
+ if (args.pluginData?.externalizeNativeAddons) return;
16
+
17
+ const result = await build.resolve(args.path, {
18
+ importer: args.importer,
19
+ kind: args.kind,
20
+ namespace: args.namespace,
21
+ resolveDir: args.resolveDir,
22
+ pluginData: {
23
+ ...args.pluginData,
24
+ externalizeNativeAddons: true,
25
+ },
26
+ });
27
+
28
+ if (result.path.endsWith('.node')) {
29
+ return {
30
+ path: args.path,
31
+ external: true,
32
+ };
33
+ }
34
+ });
35
+ },
36
+ };
37
+
38
+ /**
39
+ * Creates an esbuild incremental build context for the plugin.
40
+ * @param {object} config - Resolved plugin config
41
+ * @param {object} callbacks
42
+ * @param {() => void} callbacks.onBuildStarted - Called when a build begins
43
+ * @param {() => Promise<void>} callbacks.onBuildFinished - Called when a build ends
44
+ */
45
+ export async function createBuildContext(config, { onBuildStarted, onBuildFinished }) {
46
+ const { flags, ugepFolder, pluginName } = config;
47
+
48
+ return esbuild.context({
49
+ entryPoints: [flags.entryPoint],
50
+ minify: false,
51
+ outfile: path.resolve(ugepFolder, `./esbuild/${pluginName}.js`),
52
+ bundle: true,
53
+ sourcemap: 'inline',
54
+ sourceRoot: pathToFileURL(path.resolve(ugepFolder, 'esbuild') + path.sep).toString(),
55
+ platform: 'node',
56
+ alias: {
57
+ // Prevent esbuild from bundling react — it must be resolved at runtime.
58
+ // See: https://stackoverflow.com/a/31170775 & https://esbuild.github.io/api/#alias
59
+ react: 'react',
60
+ },
61
+ plugins: [
62
+ externalizeNativeAddonsPlugin,
63
+ esbuildPluginTsc({
64
+ force: true,
65
+ tsx: true,
66
+ tsconfigPath: path.resolve(ugepFolder, 'tsconfig.json'),
67
+ }),
68
+ {
69
+ name: 'build-events',
70
+ setup({ onEnd, onStart }) {
71
+ onEnd(onBuildFinished);
72
+ onStart(onBuildStarted);
73
+ },
74
+ },
75
+ ],
76
+ });
77
+ }
package/lib/cli.js ADDED
@@ -0,0 +1,88 @@
1
+ import { existsSync } from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ export function printUsage(exit) {
5
+ let programName;
6
+ if (/node(?:.exe)?$/.test(process.argv[0])) {
7
+ programName = `node ${path.basename(process.argv[1])}`;
8
+ } else {
9
+ programName = path.basename(process.argv[0]);
10
+ }
11
+
12
+ console.log(`
13
+ Usage:
14
+ ${programName} --ugep <ugep-file> --entry-point <js-entry-point-file>
15
+
16
+ ${programName} will encrypt and deploy files for use with GE-Shell.
17
+ Example:
18
+ ${programName} --ugep my-plugin.ugep --entry-point src/index.tsx
19
+
20
+ -u, --ugep <FILE> Deploy JS, node_modules and YAML according to ugep file
21
+ -e, --entry-point <FILE> The JS entry point file
22
+ -1, --one-shot Build once, do not watch for changes
23
+ --build-ui-only With --one-shot, build only the UI bundle
24
+ --sync-instruments-only With --one-shot, sync only instrument blobs and typings
25
+ -nd, --no-deploy Do not deploy to the plugin folder
26
+ -s, --standalone Start/restart the standalone app after each deploy and stop it on exit
27
+ `);
28
+ if (typeof exit !== 'undefined') {
29
+ process.exit(exit);
30
+ }
31
+ }
32
+
33
+ export function getFlags() {
34
+ const scriptFlags = process.argv.slice(2);
35
+ let currentArg;
36
+ const flags = { deploy: true };
37
+
38
+ while ((currentArg = scriptFlags.shift())) {
39
+ if (!currentArg.startsWith('-')) {
40
+ console.log('Unknown option:', currentArg);
41
+ printUsage(1);
42
+ } else if (currentArg === '-h' || currentArg === '--help') {
43
+ printUsage(0);
44
+ } else if (currentArg === '-u' || currentArg === '--ugep') {
45
+ flags.ugepFile = scriptFlags.shift();
46
+ if (!existsSync(flags.ugepFile)) {
47
+ console.error(`ugep file does not exist`);
48
+ process.exit(2);
49
+ }
50
+ } else if (currentArg === '-e' || currentArg === '--entry-point') {
51
+ flags.entryPoint = scriptFlags.shift();
52
+ if (!existsSync(flags.entryPoint)) {
53
+ console.error(`entry point file does not exist`);
54
+ process.exit(2);
55
+ }
56
+ } else if (currentArg === '-1' || currentArg === '--one-shot' || currentArg === '--oneshot') {
57
+ flags.oneShot = true;
58
+ } else if (currentArg === '--build-ui-only') {
59
+ flags.buildUiOnly = true;
60
+ } else if (currentArg === '--sync-instruments-only') {
61
+ flags.syncInstrumentsOnly = true;
62
+ } else if (currentArg === '-nd' || currentArg === '--no-deploy') {
63
+ flags.deploy = false;
64
+ } else if (currentArg === '-s' || currentArg === '--standalone') {
65
+ flags.standalone = true;
66
+ } else {
67
+ console.log('Unknown option:', currentArg);
68
+ printUsage(1);
69
+ }
70
+ }
71
+
72
+ if (!flags.ugepFile || !flags.entryPoint) {
73
+ console.error(`ERROR: --ugep|-u AND --entry-point|-e are mandatory`);
74
+ printUsage(1);
75
+ }
76
+
77
+ if (flags.buildUiOnly && flags.syncInstrumentsOnly) {
78
+ console.error(`ERROR: --build-ui-only and --sync-instruments-only cannot be combined`);
79
+ printUsage(1);
80
+ }
81
+
82
+ if ((flags.buildUiOnly || flags.syncInstrumentsOnly) && !flags.oneShot) {
83
+ console.error(`ERROR: --build-ui-only and --sync-instruments-only require --one-shot`);
84
+ printUsage(1);
85
+ }
86
+
87
+ return flags;
88
+ }
package/lib/config.js ADDED
@@ -0,0 +1,122 @@
1
+ import * as path from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { spawnSync } from 'node:child_process';
4
+
5
+ export const isMac = process.platform === 'darwin';
6
+
7
+ /**
8
+ * Ensures a ugep-supplied name is a single, safe path segment (no separators,
9
+ * no ".."), since it is used to build the output folder path that is later
10
+ * recursively chown'd/synced.
11
+ */
12
+ function assertSafeName(value, label) {
13
+ if (
14
+ typeof value !== 'string' ||
15
+ value.length === 0 ||
16
+ value.includes('/') ||
17
+ value.includes('\\') ||
18
+ value === '.' ||
19
+ value === '..'
20
+ ) {
21
+ throw new Error(
22
+ `Invalid ${label} "${value}" in ugep file: must be a non-empty name without path separators or "..".`,
23
+ );
24
+ }
25
+ return value;
26
+ }
27
+
28
+ function canRunBlobExportFromPath() {
29
+ const result = spawnSync('blob-export', ['-h'], { stdio: 'ignore' });
30
+ return !result.error;
31
+ }
32
+
33
+ function resolveBlobExport() {
34
+ const defaultPath = isMac
35
+ ? '/Applications/Gorilla Engine SDK/Tools/blob-export'
36
+ : `${process.env['SystemDrive']}\\Program Files\\Gorilla Engine SDK\\Tools\\blob-export.exe`;
37
+
38
+ if (existsSync(defaultPath)) {
39
+ return {
40
+ blobExportPath: defaultPath,
41
+ blobExportAvailable: true,
42
+ blobExportDefaultPath: defaultPath,
43
+ };
44
+ }
45
+
46
+ if (canRunBlobExportFromPath()) {
47
+ return {
48
+ blobExportPath: 'blob-export',
49
+ blobExportAvailable: true,
50
+ blobExportDefaultPath: defaultPath,
51
+ };
52
+ }
53
+
54
+ return {
55
+ blobExportPath: 'blob-export',
56
+ blobExportAvailable: false,
57
+ blobExportDefaultPath: defaultPath,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Creates a resolved configuration object from CLI flags and ugep file content.
63
+ * @param {object} flags - Parsed CLI flags
64
+ * @param {object} ugepContent - Parsed ugep JSON content
65
+ */
66
+ export function createConfig(flags, ugepContent) {
67
+ const ugepFolder = path.dirname(flags.ugepFile);
68
+ const { name: pluginName, manufacturer: manufacturerName } = ugepContent.pluginConfig;
69
+ assertSafeName(pluginName, 'pluginConfig.name');
70
+ assertSafeName(manufacturerName, 'pluginConfig.manufacturer');
71
+ const { blobExportPath, blobExportAvailable, blobExportDefaultPath } = resolveBlobExport();
72
+
73
+ const instrumentSourceFolder = path.resolve(ugepFolder, 'instrument');
74
+ const instrumentAssetFolder = path.resolve(
75
+ ugepFolder,
76
+ ugepContent.assetFiles.find(
77
+ ({ name, path: assetPath }) =>
78
+ name === 'instruments' || /(?:^|[\\/])instruments$/.test(assetPath),
79
+ )?.path ?? 'assets/instruments',
80
+ );
81
+ const assetSourceFolders = [
82
+ ...new Set(
83
+ (ugepContent.assetFiles ?? []).map(({ path: assetPath }) =>
84
+ path.resolve(ugepFolder, assetPath),
85
+ ),
86
+ ),
87
+ ];
88
+ const presetSourceFolders = [
89
+ ...new Set(
90
+ (ugepContent.presets ?? []).map(({ path: presetPath }) =>
91
+ path.resolve(ugepFolder, presetPath),
92
+ ),
93
+ ),
94
+ ];
95
+ const generatedInstrumentTypesPath = path.resolve(
96
+ ugepFolder,
97
+ 'src/generated/gorilla-instrument-types.d.ts',
98
+ );
99
+ const outputRoot = isMac
100
+ ? '/Library/Application Support'
101
+ : `${process.env['SystemDrive']}/ProgramData`;
102
+ const outputFolder = path.join(outputRoot, manufacturerName, pluginName);
103
+
104
+ return {
105
+ flags,
106
+ ugepContent,
107
+ ugepFolder,
108
+ pluginName,
109
+ manufacturerName,
110
+ instrumentSourceFolder,
111
+ instrumentAssetFolder,
112
+ assetSourceFolders,
113
+ presetSourceFolders,
114
+ blobExportPath,
115
+ blobExportAvailable,
116
+ blobExportDefaultPath,
117
+ generatedInstrumentTypesPath,
118
+ outputRoot,
119
+ outputFolder,
120
+ isMac,
121
+ };
122
+ }
package/lib/crypto.js ADDED
@@ -0,0 +1,59 @@
1
+ import { randomBytes, createCipheriv, createSign } from 'node:crypto';
2
+ import { copyFile, readFile, writeFile } from 'node:fs/promises';
3
+ import { resolveExistingWithinBase, ts } from './utils.js';
4
+
5
+ const algorithm = 'aes-128-gcm';
6
+
7
+ function encrypt(plaintext, key) {
8
+ const iv = randomBytes(16);
9
+ const cipher = createCipheriv(algorithm, key, iv);
10
+ const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
11
+ const tag = cipher.getAuthTag();
12
+ return Buffer.concat([iv, tag, encrypted]);
13
+ }
14
+
15
+ function sign(data, privateKey) {
16
+ const signer = createSign('RSA-SHA256');
17
+ signer.update(data);
18
+ const signature = signer.sign(privateKey);
19
+ return Buffer.concat([signature, data]);
20
+ }
21
+
22
+ async function encryptFile(src, dest, key, privateKey) {
23
+ const srcContent = await readFile(src);
24
+ await writeFile(dest, sign(encrypt(srcContent, key), privateKey));
25
+ }
26
+
27
+ /**
28
+ * Returns a function that either copies or encrypts+signs a file, depending on
29
+ * whether devRelease mode is enabled in the plugin config.
30
+ * @param {object} config - Resolved plugin config
31
+ */
32
+ export async function getDeployFunction(config) {
33
+ const { pluginConfig } = config.ugepContent;
34
+
35
+ if (pluginConfig.devRelease_1c327a6c414fa896 === true) {
36
+ return (src, dest) => {
37
+ console.log(`[${ts()}] Copying ${src} to ${dest}`);
38
+ return copyFile(src, dest);
39
+ };
40
+ }
41
+
42
+ const keyPath = await resolveExistingWithinBase(
43
+ config.ugepFolder,
44
+ pluginConfig.installerOutputDir,
45
+ 'jsEncryptionKey.bin',
46
+ );
47
+ const privateKeyPath = await resolveExistingWithinBase(
48
+ config.ugepFolder,
49
+ pluginConfig.installerOutputDir,
50
+ 'signingPrivateKey.pem',
51
+ );
52
+ const key = await readFile(keyPath);
53
+ const privateKey = await readFile(privateKeyPath);
54
+
55
+ return (src, dest) => {
56
+ console.log(`[${ts()}] Encrypting ${src} to ${dest}`);
57
+ return encryptFile(src, dest, key, privateKey);
58
+ };
59
+ }
@@ -0,0 +1,28 @@
1
+ import { realpathSync, statSync } from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { assertSafePathSegment, resolveWithinBase } from './utils.js';
4
+
5
+ export function resolveInstallerPath(ugepFile, pluginConfig, extension) {
6
+ const installerName = pluginConfig.installerNameTemplate
7
+ .replace('{manufacturer}', pluginConfig.manufacturer)
8
+ .replace('{name}', pluginConfig.name)
9
+ .replace('{version}', pluginConfig.version);
10
+ assertSafePathSegment(installerName, 'installer name');
11
+ assertSafePathSegment(extension, 'installer extension');
12
+
13
+ const ugepDir = path.dirname(path.resolve(ugepFile));
14
+ const installerDir = resolveWithinBase(
15
+ ugepDir,
16
+ pluginConfig.installerOutputDir || 'installer',
17
+ );
18
+ const installerPath = resolveWithinBase(installerDir, `${installerName}.${extension}`);
19
+
20
+ const realUgepDir = realpathSync(ugepDir);
21
+ const realInstallerPath = realpathSync(installerPath);
22
+ resolveWithinBase(realUgepDir, path.relative(realUgepDir, realInstallerPath));
23
+ if (!statSync(realInstallerPath).isFile()) {
24
+ throw new Error(`Installer is not a regular file: ${installerPath}`);
25
+ }
26
+
27
+ return { installerName, installerPath: realInstallerPath };
28
+ }