dazscript-framework 1.0.2 → 1.0.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/README.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  **DazScript Framework** is a TypeScript toolkit for writing [Daz Studio](https://www.daz3d.com/daz-studio) scripts. It layers a full TypeScript development experience on top of [DAZ Script](https://docs.daz3d.com/public/software/dazstudio/4/referenceguide/scripting/start) (Qt Script / ECMAScript 5.1), and ships a fluent dialog builder so you can build UIs in code without touching the Qt widget API directly.
4
4
 
5
+ ## Table of Contents
6
+
7
+ - [Why use it?](#why-use-it)
8
+ - [Quick Start: Hello World](#quick-start-hello-world)
9
+ - [Quick Start: A Simple Dialog](#quick-start-a-simple-dialog)
10
+ - [Documentation](#documentation)
11
+ - [Installation & Setup](#installation--setup)
12
+ - [Project Configuration](#project-configuration)
13
+ - [The `action(...)` Entrypoint](#the-action-entrypoint)
14
+ - [Build Output: Launcher Shims](#build-output-launcher-shims)
15
+ - [Generated Setup Script](#generated-setup-script)
16
+ - [Setup Keyboard Shortcuts](#setup-keyboard-shortcuts)
17
+ - [Action-Level Bundles](#action-level-bundles)
18
+ - [Building UIs: Dialogs & Observables](#building-uis-dialogs--observables)
19
+ - [Observables](#observables)
20
+ - [Dialog Builder Reference](#dialog-builder-reference)
21
+ - [Available Helpers](#available-helpers)
22
+ - [Directory Structure](#directory-structure)
23
+ - [Development & Publishing](#development--publishing)
24
+ - [Resources](#resources)
25
+ - [Examples](#examples)
26
+
5
27
  ## Why use it?
6
28
 
7
29
  DAZ Script gives you direct access to the entire Daz Studio API. The DazScript Framework builds on that foundation and adds:
@@ -226,7 +248,15 @@ Each built action produces two files:
226
248
 
227
249
  When you rebuild, only the implementation bundle changes. The launcher path stays stable, so re-registering the action in Daz Studio is normally not required.
228
250
 
229
- At runtime the launcher looks for the local `lib/` bundle first, then falls back to `App.getAppDataPath()/<appDataPath>`.
251
+ At runtime the launcher looks for a local encrypted `script.dse` bundle first, then the local `script.dsa` bundle, then the same encrypted/plain fallback paths under `App.getAppDataPath()/<appDataPath>`.
252
+
253
+ To encrypt implementation bundles before packaging, run the build and then call Daz Studio through the framework CLI:
254
+
255
+ ```bash
256
+ npx dazscript encrypt --out-dir ./out --daz-studio "C:/Program Files/DAZ 3D/DAZStudio4/DAZStudio.exe"
257
+ ```
258
+
259
+ The `encrypt` command launches Daz Studio with `-headless -noPrompt`, converts each `out/**/lib/**/script.dsa` to `script.dse`, and deletes the source `script.dsa` after the matching encrypted file is written. Add `--keep-source` to leave the plain implementation bundles in place.
230
260
 
231
261
  ---
232
262
 
@@ -254,7 +284,7 @@ Applying the dialog:
254
284
  - Affected toolbars are rebuilt; empty framework-created toolbars are removed
255
285
  - Selected keyboard shortcut rows are applied after actions are installed
256
286
 
257
- This replaces the older `Install.dsa.ts` / `Uninstall.dsa.ts` pattern. The installer generator removes those legacy files if they exist, except when shortcut restoration is needed.
287
+ This replaces the older `Install.dsa.ts` / `Uninstall.dsa.ts` pattern. The installer generator removes those legacy files if they exist.
258
288
 
259
289
  ### Setup Keyboard Shortcuts
260
290
 
@@ -288,7 +318,7 @@ Before changing a non-custom Daz Studio action shortcut, setup writes the origin
288
318
  App.getAppDataPath()/<appDataPath>/Installer/keyboard-shortcuts-backup.json
289
319
  ```
290
320
 
291
- When shortcut JSON exists, the generator also writes `src/Uninstall.dsa.ts`. Running that uninstall script restores backed-up non-custom shortcuts. Custom action shortcuts are not backed up because uninstalling the custom action removes the shortcut with the action.
321
+ Shortcut setup is part of the generated setup dialog. The generator does not create a separate shortcut-only uninstall script.
292
322
 
293
323
  ---
294
324
 
@@ -6,6 +6,7 @@ const readline = require('readline');
6
6
  const { runWebpack } = require('./build');
7
7
  const { loadConfig } = require('./config-loader');
8
8
  const { copyIcons } = require('./icons');
9
+ const { runEncrypt } = require('./encrypt');
9
10
  const { generateInstallerFiles } = require('./install-generator');
10
11
  const { initProject } = require('./init');
11
12
 
@@ -16,14 +17,18 @@ Commands:
16
17
  init Scaffold a DazScript project in the current directory
17
18
  build Build DazScript files
18
19
  watch Build and watch DazScript files
20
+ encrypt Encrypt built implementation bundles through Daz Studio
19
21
  icons Copy png assets into the output directory
20
22
  installer Generate Install.dsa.ts and Uninstall.dsa.ts
21
23
 
22
- Options for init:
24
+ Options:
23
25
  --menu-path <path> Default menu path. Default: /MyScripts
24
26
  --scripts-path <path> Source directory to scan. Default: ./src
25
27
  --out-dir <path> Build output directory. Default: ./out
26
28
  --app-data-path <path> AppData namespace used by launcher fallbacks. Example: Author/Product
29
+ --daz-studio <path> Daz Studio executable for encrypt
30
+ --keep-source Keep source script.dsa files after encrypting
31
+ --timeout-ms <value> Daz Studio encrypt timeout. Default: 300000
27
32
  --force Overwrite generated files
28
33
  --help Show this message
29
34
  `);
@@ -110,6 +115,23 @@ function parseOptions(args, defaults) {
110
115
  continue;
111
116
  }
112
117
 
118
+ if (arg === '--daz-studio') {
119
+ options.dazStudio = args[index + 1];
120
+ index += 1;
121
+ continue;
122
+ }
123
+
124
+ if (arg === '--keep-source') {
125
+ options.keepSource = true;
126
+ continue;
127
+ }
128
+
129
+ if (arg === '--timeout-ms') {
130
+ options.timeoutMs = Number(args[index + 1]);
131
+ index += 1;
132
+ continue;
133
+ }
134
+
113
135
  if (arg === '--file') {
114
136
  options.file = args[index + 1];
115
137
  index += 1;
@@ -158,6 +180,9 @@ async function main(argv) {
158
180
  outDir: undefined,
159
181
  appDataPath: undefined,
160
182
  file: undefined,
183
+ dazStudio: undefined,
184
+ keepSource: false,
185
+ timeoutMs: undefined,
161
186
  });
162
187
 
163
188
  if (options.help) {
@@ -192,6 +217,11 @@ async function main(argv) {
192
217
  return;
193
218
  }
194
219
 
220
+ if (command === 'encrypt') {
221
+ runEncrypt(workdir, resolvedOptions);
222
+ return;
223
+ }
224
+
195
225
  if (command === 'icons') {
196
226
  copyIcons(workdir, resolvedOptions);
197
227
  return;
@@ -0,0 +1,179 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ function toDazPath(filePath) {
9
+ return path.resolve(filePath).replace(/\\/g, '/');
10
+ }
11
+
12
+ function findImplementationScripts(dir) {
13
+ const scripts = [];
14
+
15
+ function visit(currentDir) {
16
+ if (!fs.existsSync(currentDir)) {
17
+ return;
18
+ }
19
+
20
+ for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
21
+ const entryPath = path.join(currentDir, entry.name);
22
+
23
+ if (entry.isDirectory()) {
24
+ visit(entryPath);
25
+ continue;
26
+ }
27
+
28
+ if (
29
+ entry.isFile() &&
30
+ entry.name === 'script.dsa' &&
31
+ path.basename(path.dirname(entryPath)) !== 'lib' &&
32
+ entryPath.split(path.sep).includes('lib')
33
+ ) {
34
+ scripts.push(entryPath);
35
+ }
36
+ }
37
+ }
38
+
39
+ visit(dir);
40
+ return scripts;
41
+ }
42
+
43
+ function makeConverterScript() {
44
+ return [
45
+ '(function(){',
46
+ ' function log(message) { print("[dazscript encrypt] " + message); }',
47
+ ' function fail(message) {',
48
+ ' print("[dazscript encrypt] ERROR " + message);',
49
+ ' App.quit();',
50
+ ' }',
51
+ ' function splitList(value) {',
52
+ ' if (!value) return [];',
53
+ ' return String(value).split("|");',
54
+ ' }',
55
+ ' function removeSourceFile(sourcePath) {',
56
+ ' var sourceFile = new DzFile(sourcePath);',
57
+ ' if (sourceFile.exists() && !sourceFile.remove()) {',
58
+ ' log("warning: encrypted but could not delete " + sourcePath);',
59
+ ' }',
60
+ ' sourceFile.deleteLater();',
61
+ ' }',
62
+ ' var args = App.scriptArgs;',
63
+ ' var sourceList = splitList(args[0]);',
64
+ ' var deleteSources = String(args[1]) == "delete";',
65
+ ' if (!sourceList.length) {',
66
+ ' log("no implementation scripts found");',
67
+ ' App.quit();',
68
+ ' return;',
69
+ ' }',
70
+ ' var failures = 0;',
71
+ ' for (var i = 0; i < sourceList.length; i += 1) {',
72
+ ' var sourcePath = sourceList[i];',
73
+ ' var destPath = sourcePath.replace(/\\.dsa$/i, ".dse");',
74
+ ' var script = new DzScript();',
75
+ ' log("encrypt " + sourcePath);',
76
+ ' if (!script.loadFromFile(sourcePath)) {',
77
+ ' failures += 1;',
78
+ ' log("failed to load " + sourcePath);',
79
+ ' script.deleteLater();',
80
+ ' continue;',
81
+ ' }',
82
+ ' if (!script.checkSyntax()) {',
83
+ ' failures += 1;',
84
+ ' log("syntax error in " + sourcePath + " line " + script.errorLine() + ": " + script.errorMessage());',
85
+ ' script.deleteLater();',
86
+ ' continue;',
87
+ ' }',
88
+ ' var writeError = script.saveFile(destPath, DzScript.EncDAZScriptFile);',
89
+ ' script.deleteLater();',
90
+ ' if (Number(writeError) != 0) {',
91
+ ' failures += 1;',
92
+ ' log("failed to save " + destPath + " error=" + writeError);',
93
+ ' continue;',
94
+ ' }',
95
+ ' if (deleteSources) {',
96
+ ' removeSourceFile(sourcePath);',
97
+ ' }',
98
+ ' log("wrote " + destPath);',
99
+ ' }',
100
+ ' if (failures) {',
101
+ ' fail(String(failures) + " script(s) failed");',
102
+ ' return;',
103
+ ' }',
104
+ ' log("complete");',
105
+ ' App.quit();',
106
+ '})();',
107
+ '',
108
+ ].join('\n');
109
+ }
110
+
111
+ function runEncrypt(workdir, options) {
112
+ if (!options.dazStudio) {
113
+ throw new Error('Missing required option: --daz-studio <path>');
114
+ }
115
+
116
+ const outDir = path.resolve(workdir, options.outDir || './out');
117
+ const dazStudioPath = path.resolve(workdir, options.dazStudio);
118
+
119
+ if (!fs.existsSync(dazStudioPath)) {
120
+ throw new Error(`Daz Studio executable not found: ${dazStudioPath}`);
121
+ }
122
+
123
+ if (!fs.existsSync(outDir)) {
124
+ throw new Error(`Output directory not found: ${outDir}`);
125
+ }
126
+
127
+ const implementationScripts = findImplementationScripts(outDir);
128
+ if (!implementationScripts.length) {
129
+ console.log(`[dazscript encrypt] no implementation scripts found under ${outDir}`);
130
+ return;
131
+ }
132
+
133
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dazscript-encrypt-'));
134
+ const converterPath = path.join(tempDir, 'encrypt-output.dsa');
135
+ fs.writeFileSync(converterPath, makeConverterScript(), 'utf8');
136
+
137
+ const args = [
138
+ '-headless',
139
+ '-noPrompt',
140
+ '-scriptArg',
141
+ implementationScripts.map(toDazPath).join('|'),
142
+ '-scriptArg',
143
+ options.keepSource ? 'keep' : 'delete',
144
+ toDazPath(converterPath),
145
+ ];
146
+
147
+ console.log(`[dazscript encrypt] encrypting ${implementationScripts.length} implementation script(s)`);
148
+ const result = spawnSync(dazStudioPath, args, {
149
+ encoding: 'utf8',
150
+ stdio: 'inherit',
151
+ timeout: options.timeoutMs || 300000,
152
+ });
153
+
154
+ fs.rmSync(tempDir, { recursive: true, force: true });
155
+
156
+ if (result.error) {
157
+ throw result.error;
158
+ }
159
+
160
+ if (result.status !== 0) {
161
+ throw new Error(`Daz Studio exited with code ${result.status}`);
162
+ }
163
+
164
+ const missingOutputs = implementationScripts.filter((scriptPath) => {
165
+ const encryptedPath = scriptPath.replace(/\.dsa$/i, '.dse');
166
+ return !fs.existsSync(encryptedPath);
167
+ });
168
+
169
+ if (missingOutputs.length) {
170
+ throw new Error(
171
+ 'Daz Studio did not produce encrypted output for:\n' +
172
+ missingOutputs.map((scriptPath) => ` ${scriptPath}`).join('\n')
173
+ );
174
+ }
175
+ }
176
+
177
+ module.exports = {
178
+ runEncrypt,
179
+ };
@@ -29,14 +29,6 @@ setup(${data}, ${JSON.stringify(options)});
29
29
  `;
30
30
  }
31
31
 
32
- function generateUninstallTemplate(options) {
33
- return `
34
- import { restoreSetupKeyboardShortcuts as restoreShortcuts } from '@dsf/helpers/custom-action-installer-helper';
35
-
36
- restoreShortcuts(${JSON.stringify(options)});
37
- `;
38
- }
39
-
40
32
  function literalValue(node) {
41
33
  if (!node) {
42
34
  return undefined;
@@ -329,9 +321,7 @@ function generateInstallerFiles(workdir, options) {
329
321
  const uninstallPath = path.join(workdir, 'src', 'Uninstall.dsa.ts');
330
322
  if (fs.existsSync(installPath)) fs.unlinkSync(installPath);
331
323
 
332
- if (shortcutData.shortcuts && shortcutData.shortcuts.length > 0) {
333
- fs.writeFileSync(path.join(workdir, 'src', 'Uninstall.dsa.ts'), generateUninstallTemplate(setupOptions));
334
- } else if (fs.existsSync(uninstallPath)) {
324
+ if (fs.existsSync(uninstallPath)) {
335
325
  fs.unlinkSync(uninstallPath);
336
326
  }
337
327
  }
@@ -16,17 +16,20 @@ function getActionOutputPath(workdir, outDir, sourceFile) {
16
16
  return relativeSourceFile.replace(/\.ts$/, '');
17
17
  }
18
18
 
19
- function getImplementationRelativePath(outputRelativePath) {
19
+ function getImplementationRelativePath(outputRelativePath, extension = 'dsa') {
20
20
  const outputDirectory = path.posix.dirname(outputRelativePath);
21
21
  const outputBaseName = path.posix.basename(outputRelativePath, '.dsa');
22
22
  const implementationDirectory = outputDirectory === '.'
23
23
  ? path.posix.join('lib', outputBaseName)
24
24
  : path.posix.join(outputDirectory, 'lib', outputBaseName);
25
25
 
26
- return path.posix.join(implementationDirectory, 'script.dsa');
26
+ return path.posix.join(implementationDirectory, `script.${extension}`);
27
27
  }
28
28
 
29
29
  function makeLauncherSource(implementationRelativePath, appDataImplementationRelativePath) {
30
+ const encryptedImplementationRelativePath = implementationRelativePath.replace(/\.dsa$/, '.dse');
31
+ const encryptedAppDataImplementationRelativePath = appDataImplementationRelativePath.replace(/\.dsa$/, '.dse');
32
+
30
33
  return [
31
34
  '// Auto-generated launcher shim.',
32
35
  '// The installed Daz action points at this stable file.',
@@ -34,7 +37,9 @@ function makeLauncherSource(implementationRelativePath, appDataImplementationRel
34
37
  '// and from the AppData fallback location second.',
35
38
  '',
36
39
  `var implementationRelativePath = '${implementationRelativePath}';`,
40
+ `var encryptedImplementationRelativePath = '${encryptedImplementationRelativePath}';`,
37
41
  `var appDataImplementationRelativePath = '${appDataImplementationRelativePath}';`,
42
+ `var encryptedAppDataImplementationRelativePath = '${encryptedAppDataImplementationRelativePath}';`,
38
43
  'var launcherFileName = getScriptFileName();',
39
44
  'var launcherInfo = new DzFileInfo(launcherFileName);',
40
45
  'var launcherDirectory = typeof launcherInfo.canonicalPath == "function"',
@@ -42,10 +47,20 @@ function makeLauncherSource(implementationRelativePath, appDataImplementationRel
42
47
  ' : launcherInfo.path();',
43
48
  'launcherInfo.deleteLater();',
44
49
  '',
45
- "var implementationPath = launcherDirectory + '/' + implementationRelativePath;",
50
+ "var implementationPath = launcherDirectory + '/' + encryptedImplementationRelativePath;",
46
51
  'var implementationFile = new DzFile(implementationPath);',
47
52
  'if (!implementationFile.exists()) {',
48
53
  ' implementationFile.deleteLater();',
54
+ " implementationPath = launcherDirectory + '/' + implementationRelativePath;",
55
+ ' implementationFile = new DzFile(implementationPath);',
56
+ '}',
57
+ 'if (!implementationFile.exists()) {',
58
+ ' implementationFile.deleteLater();',
59
+ " implementationPath = App.getAppDataPath() + '/' + encryptedAppDataImplementationRelativePath;",
60
+ ' implementationFile = new DzFile(implementationPath);',
61
+ '}',
62
+ 'if (!implementationFile.exists()) {',
63
+ ' implementationFile.deleteLater();',
49
64
  " implementationPath = App.getAppDataPath() + '/' + appDataImplementationRelativePath;",
50
65
  ' implementationFile = new DzFile(implementationPath);',
51
66
  '}',
@@ -86,6 +101,10 @@ function createActionLaunchers(workdir, options) {
86
101
  const launcherPath = path.join(outDir, outputRelativePath);
87
102
  const implementationRelativePath = getImplementationRelativePath(outputRelativePath);
88
103
  const implementationPath = path.join(outDir, implementationRelativePath);
104
+ const encryptedImplementationPath = path.join(
105
+ outDir,
106
+ getImplementationRelativePath(outputRelativePath, 'dse')
107
+ );
89
108
 
90
109
  if (!fs.existsSync(launcherPath)) {
91
110
  return;
@@ -95,6 +114,9 @@ function createActionLaunchers(workdir, options) {
95
114
  if (fs.existsSync(implementationPath)) {
96
115
  fs.unlinkSync(implementationPath);
97
116
  }
117
+ if (fs.existsSync(encryptedImplementationPath)) {
118
+ fs.unlinkSync(encryptedImplementationPath);
119
+ }
98
120
 
99
121
  fs.renameSync(launcherPath, implementationPath);
100
122
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dazscript-framework",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "author": "Freddy Diaz",
5
5
  "license": "MPL-2.0",
6
6
  "description": "",
@@ -78,6 +78,10 @@ export type CustomActionTargets = {
78
78
  toolbar: boolean
79
79
  }
80
80
 
81
+ type ApplyCustomActionOptions = {
82
+ deferToolbar?: boolean
83
+ }
84
+
81
85
  type CustomActionCandidate = {
82
86
  name: string
83
87
  text: string
@@ -377,13 +381,18 @@ export const getInstalledCustomActionState = (action: CustomAction, scriptsPath:
377
381
  }
378
382
  }
379
383
 
380
- export const applyCustomActionTargets = (action: CustomAction, targets: CustomActionTargets, scriptsPath: string = getScriptPath()) => {
384
+ export const applyCustomActionTargets = (
385
+ action: CustomAction,
386
+ targets: CustomActionTargets,
387
+ scriptsPath: string = getScriptPath(),
388
+ options: ApplyCustomActionOptions = {}
389
+ ): CustomAction | null => {
381
390
  const resolvedAction = resolveActionPaths(action, scriptsPath)
382
391
  const shouldInstall = targets.menu || targets.toolbar
383
392
 
384
393
  if (!shouldInstall) {
385
394
  removeCustomActionTargets(action, { menu: true, toolbar: true }, scriptsPath)
386
- return
395
+ return null
387
396
  }
388
397
 
389
398
  const customAction = createOrUpdateCustomAction(action, resolvedAction)
@@ -392,9 +401,11 @@ export const applyCustomActionTargets = (action: CustomAction, targets: CustomAc
392
401
  addToMenu(customAction)
393
402
  }
394
403
 
395
- if (targets.toolbar && action.toolbar) {
404
+ if (targets.toolbar && action.toolbar && !options.deferToolbar) {
396
405
  addToToolbar(customAction)
397
406
  }
407
+
408
+ return customAction
398
409
  }
399
410
 
400
411
  export const removeCustomActionTargets = (action: CustomAction, targets: CustomActionTargets, scriptsPath: string = getScriptPath()) => {
@@ -561,24 +561,31 @@ export const showSetupCustomActionsDialog = (actions: CustomAction[], options: s
561
561
 
562
562
  debug(`[Setup] applying ${selections.actions.filter(selection => selection.selected).length}/${selections.actions.length} selected actions`)
563
563
 
564
- const removedToolbarNames = new CustomSet<string>()
564
+ const touchedToolbarNames = new CustomSet<string>()
565
+ const selectedToolbarActions: CustomAction[] = []
565
566
 
566
567
  progress('Setting Up Scripts', selections.actions, (selection) => {
567
568
  debug(`[Setup] ${selection.selected ? 'install' : 'remove'} "${selection.action.text}"`)
568
569
 
570
+ if (selection.supportsToolbar && selection.action.toolbar) {
571
+ touchedToolbarNames.add(selection.action.toolbar)
572
+ }
573
+
569
574
  if (selection.selected) {
570
- applyCustomActionTargets({
575
+ const customAction = applyCustomActionTargets({
571
576
  ...selection.action,
572
577
  shortcut: selection.effectiveShortcut
573
578
  }, {
574
579
  menu: selection.supportsMenu,
575
580
  toolbar: selection.supportsToolbar
581
+ }, undefined, {
582
+ deferToolbar: true
576
583
  })
577
- return
578
- }
579
584
 
580
- if (selection.supportsToolbar && selection.action.toolbar) {
581
- removedToolbarNames.add(selection.action.toolbar)
585
+ if (customAction && selection.supportsToolbar && selection.action.toolbar) {
586
+ selectedToolbarActions.push(customAction)
587
+ }
588
+ return
582
589
  }
583
590
 
584
591
  removeCustomActionTargets(selection.action, {
@@ -587,14 +594,11 @@ export const showSetupCustomActionsDialog = (actions: CustomAction[], options: s
587
594
  })
588
595
  })
589
596
 
590
- removedToolbarNames.forEach((toolbarName) => {
597
+ touchedToolbarNames.forEach((toolbarName) => {
591
598
  clearToolbar(toolbarName)
592
599
 
593
- const stillSelected = selections.actions.filter(s => s.selected && s.supportsToolbar && s.action.toolbar === toolbarName)
594
- stillSelected.forEach((s) => addToToolbar({
595
- ...s.action,
596
- shortcut: s.effectiveShortcut
597
- }))
600
+ const stillSelected = selectedToolbarActions.filter(action => action.toolbar === toolbarName)
601
+ stillSelected.forEach((action) => addToToolbar(action))
598
602
  debug(`[Setup] Toolbar ${toolbarName} rebuilt with ${stillSelected.length} remaining actions`)
599
603
 
600
604
  if (stillSelected.length === 0) {