@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/LICENSE.md +64 -0
- package/README.md +107 -0
- package/build.js +351 -0
- package/compiler-detection.js +50 -0
- package/deploy.js +93 -0
- package/lib/assets.js +188 -0
- package/lib/build.js +77 -0
- package/lib/cli.js +88 -0
- package/lib/config.js +122 -0
- package/lib/crypto.js +59 -0
- package/lib/installer.js +28 -0
- package/lib/instruments.js +355 -0
- package/lib/permissions.js +121 -0
- package/lib/repair-windows-permissions.ps1 +8 -0
- package/lib/standalone.js +44 -0
- package/lib/utils.js +89 -0
- package/lib/watchers.js +59 -0
- package/package.json +37 -0
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
|
+
}
|
package/lib/installer.js
ADDED
|
@@ -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
|
+
}
|