dazscript-framework 0.2.4 → 0.3.1

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
@@ -13,7 +13,9 @@ The **DazScript Framework** is a TypeScript-based framework for writing Daz Stud
13
13
  ## Features
14
14
 
15
15
  - TypeScript support with full IntelliSense.
16
- - A powerful set of decorators and helper methods for building interactive scripts.
16
+ - A lightweight `action(...)` entrypoint plus helper methods for building interactive scripts.
17
+ - A generated setup dialog for installing, updating, and removing custom action registrations.
18
+ - Setup generation is fully automated from `action(...)` metadata, including menu path, toolbar, shortcut, description, grouping, icons, and bundle-based setup outputs.
17
19
  - Easy integration with Daz Studio for quick script deployment.
18
20
 
19
21
  ## Installation
@@ -32,6 +34,8 @@ After installing the package, scaffold the project files:
32
34
  npx dazscript init
33
35
  ```
34
36
 
37
+ If `--app-data-path` is not provided, `init` prompts for the AppData author namespace up front and uses the current folder name as the default product segment.
38
+
35
39
  This generates:
36
40
 
37
41
  - `dazscript.config.ts`
@@ -43,15 +47,39 @@ The generated package scripts use the framework CLI directly, so consumer projec
43
47
  You can customize the generated defaults:
44
48
 
45
49
  ```bash
46
- npx dazscript init --menu-path /MyScripts --scripts-path ./src --out-dir ./out
50
+ npx dazscript init --menu-path /MyScripts --scripts-path ./src --out-dir ./out --app-data-path YourName/my-project
47
51
  ```
48
52
 
49
53
  - `--menu-path` sets which Daz Studio menu the scripts are added to by default. See [The `action(...)` Entrypoint](#the-action-entrypoint) for how a script can override that with `menuPath`.
50
54
  - `--scripts-path` tells the installer generator where to scan for runnable `.dsa.ts` entry files.
51
55
  - `--out-dir` sets the webpack build output directory for generated `.dsa` files and copied icons.
56
+ - `--app-data-path` sets the AppData namespace used by launcher fallback resolution. Use a unique `Author/Product` path.
52
57
 
53
58
  Use `--scripts-path ./src/scripts` for projects shaped like `scripts/common`, where runnable `.dsa.ts` files live under `src/scripts/`. Use `--scripts-path ./src` for packages shaped like `scripts/power-menu`, where runnable `.dsa.ts` files live at the source root.
54
59
 
60
+ Set `appDataPath` explicitly in `dazscript.config.ts` for every project. It is required for builds that generate launcher shims:
61
+
62
+ ```typescript
63
+ import { defineConfig } from 'dazscript-framework/config';
64
+
65
+ export default defineConfig({
66
+ scriptsPath: './src',
67
+ outDir: './out',
68
+ defaultMenuPath: '/MyScripts',
69
+ appDataPath: 'YourName/my-project',
70
+ bundleName: 'My Project',
71
+ });
72
+ ```
73
+
74
+ `bundleName` is optional display metadata for generated setup dialogs. If omitted, the dialog falls back to `Setup Scripts`. `dazscript init` now scaffolds it automatically from the project folder name.
75
+
76
+ Built action outputs now use stable launcher shims by default:
77
+
78
+ - `out/<script>.dsa` is the stable launcher registered with Daz Studio menus, toolbars, and shortcuts
79
+ - `out/<folder>/lib/<script-name>/script.dsa` is the current implementation bundle that the launcher executes
80
+
81
+ Rebuilding updates the implementation bundle under the shim's sibling `lib/` folder. At runtime, each launcher checks that local `lib/` path first and falls back to `App.getAppDataPath()/...` second. Because the registered launcher path stays stable, action updates normally do not require reinstalling the action in Daz Studio.
82
+
55
83
  ## Usage
56
84
 
57
85
  ### Quick Start: Hello World
@@ -106,7 +134,67 @@ Common `action(...)` parameters:
106
134
  - `toolbar`: the toolbar name used when the action should appear on a toolbar.
107
135
  - `group`: an optional grouping label used by Daz Studio for related actions.
108
136
  - `description`: a longer description for the action.
109
- - `bundle`: generates installer and uninstaller entries as a package bundle instead of a single action entry.
137
+ - `bundle`: generates an additional setup script next to the action file. Use `true` for `Setup.dsa.ts` or a string for `Setup <bundle>.dsa.ts`.
138
+
139
+ When an action is built, the framework emits two files for it:
140
+
141
+ - the stable launcher at the original output path
142
+ - the implementation bundle under a sibling `lib/<script-name>/script.dsa` path
143
+
144
+ Generated installers register the launcher path, so menu placement, toolbars, shortcuts, and icons keep pointing at a stable target across rebuilds.
145
+
146
+ If the local `lib/` implementation is missing, the launcher falls back to the configured `appDataPath`. Builds now require this value and validate it as a unique `Author/Product` style path.
147
+
148
+ ### Generated Setup Script
149
+
150
+ Running `npm run installer` generates `src/Setup.dsa.ts` for the project.
151
+
152
+ This flow is completely automated. The installer generator scans runnable `.dsa.ts` entry files, reads the top-level `action(...)` call, and derives the setup dialog rows and registration behavior directly from that metadata. In practice, the menu path, toolbar target, shortcut, description, grouping, icon usage, and bundle-specific setup outputs all come from the action definition rather than from separate installer code you have to maintain by hand.
153
+
154
+ The generated setup script:
155
+
156
+ - Scans all runnable top-level `.dsa.ts` files under `scriptsPath`
157
+ - Reads `action(...)` metadata directly from the source
158
+ - Normalizes default menu paths relative to `defaultMenuPath`
159
+ - Derives action labels, descriptions, shortcuts, toolbar targets, grouping, and icons from the action definition
160
+ - Writes one searchable setup entry per discovered action
161
+ - Uses `appDataPath/Installer` as the installer settings namespace
162
+ - Passes `bundleName` through so the dialog title can be project-specific
163
+
164
+ The setup dialog initializes from the current Daz Studio install state rather than assuming a clean install. It checks which actions are already installed, which ones are present in menus or toolbars, and what shortcut is currently assigned.
165
+
166
+ Current setup dialog behavior:
167
+
168
+ - Shows an install checkbox plus the columns `Action`, `Shortcut`, `Description`, `Menu`, and `Toolbar`
169
+ - Includes a search box that filters by action name, shortcut, description, menu path, and toolbar
170
+ - Supports `Select All` and `Deselect All` for the currently visible rows
171
+ - Lets the user right-click an action to set a shortcut or reset it to the default shortcut
172
+ - Shows shortcut overrides with an `[ovr]` marker
173
+ - Displays the configured toolbar name directly instead of a generic yes/no flag
174
+ - Uses the configured `bundleName` in the window title when available
175
+
176
+ Applying the setup dialog does both install and cleanup work:
177
+
178
+ - Selected rows are installed or updated through the framework custom action helpers
179
+ - Unselected rows are removed from supported menu and toolbar targets
180
+ - Affected toolbars are rebuilt after removal so remaining selected actions stay grouped correctly
181
+ - Empty toolbars created by the framework are cleaned up automatically
182
+
183
+ The generated project-level setup file replaces the older generated `Install.dsa.ts` and `Uninstall.dsa.ts` flow. The installer generator now removes those legacy files if they still exist.
184
+
185
+ ### Action-Level Bundles
186
+
187
+ The `bundle` property on `action(...)` is separate from project `bundleName`.
188
+
189
+ - `bundleName` in `dazscript.config.ts` is project metadata used for the setup dialog title
190
+ - `bundle` in an action definition changes installer generation behavior for that action
191
+
192
+ When `bundle` is set on an action, the installer generator also writes a setup script beside that action:
193
+
194
+ - `bundle: true` writes `Setup.dsa.ts`
195
+ - `bundle: 'Utilities'` writes `Setup Utilities.dsa.ts`
196
+
197
+ Those bundle-generated setup files use the same setup dialog helper and now also receive the project `bundleName`.
110
198
 
111
199
  ### Building UIs with Observables & Dialogs
112
200
 
@@ -302,7 +390,7 @@ my-daz-scripts/
302
390
  │ │ ├── my-dialog.ts
303
391
  │ │ └── my-dialog-script.dsa.ts
304
392
  │ └── config.ts
305
- ├── out/ # Generated .dsa files (build output)
393
+ ├── out/ # Generated launchers, implementations, and copied icons
306
394
  ├── package.json
307
395
  ├── tsconfig.json
308
396
  └── dazscript.config.ts
@@ -311,8 +399,10 @@ my-daz-scripts/
311
399
  **Key points:**
312
400
  - Scripts ending in `.dsa.ts` compile to `.dsa` files for Daz Studio
313
401
  - Regular `.ts` files are utility, model, or helper classes
402
+ - Built action outputs are split into stable launchers plus sibling `lib/<script-name>/script.dsa` implementations
314
403
  - Run `npm run build` to compile TypeScript → Daz Scripts
315
404
  - Run `npm run watch` during development for live rebuild
405
+ - Rebuild after script changes; reinstalling Daz actions is usually not required because the launcher path stays stable
316
406
 
317
407
  ## Development & Publishing
318
408
 
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ function toPosix(filePath) {
4
+ return filePath.replace(/\\/g, '/');
5
+ }
6
+
7
+ function validateAppDataPath(appDataPath, workdir) {
8
+ if (!appDataPath || typeof appDataPath !== 'string') {
9
+ throw new Error(
10
+ `[dazscript] Missing required appDataPath in ${workdir}. ` +
11
+ `Set appDataPath: 'Author/Product' in dazscript.config.ts.`
12
+ );
13
+ }
14
+
15
+ const normalized = toPosix(appDataPath).trim().replace(/^\/+|\/+$/g, '');
16
+ const segments = normalized.split('/').filter(Boolean);
17
+
18
+ if (segments.length < 2) {
19
+ throw new Error(
20
+ `[dazscript] Invalid appDataPath "${appDataPath}" in ${workdir}. ` +
21
+ `Use at least two segments, for example 'Author/Product'.`
22
+ );
23
+ }
24
+
25
+ const blockedSegments = new Set([
26
+ 'appdata',
27
+ 'cache',
28
+ 'data',
29
+ 'lib',
30
+ 'libs',
31
+ 'script',
32
+ 'scripts',
33
+ 'temp',
34
+ 'tmp',
35
+ ]);
36
+
37
+ const invalidSegment = segments.find((segment) => blockedSegments.has(segment.toLowerCase()));
38
+ if (invalidSegment) {
39
+ throw new Error(
40
+ `[dazscript] Invalid appDataPath "${appDataPath}" in ${workdir}. ` +
41
+ `Path segments like "${invalidSegment}" are too generic. Use a unique Author/Product path.`
42
+ );
43
+ }
44
+
45
+ return normalized;
46
+ }
47
+
48
+ module.exports = {
49
+ validateAppDataPath,
50
+ };
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ const path = require('path');
5
+ const readline = require('readline');
4
6
  const { runWebpack } = require('./build');
5
7
  const { loadConfig } = require('./config-loader');
6
8
  const { copyIcons } = require('./icons');
@@ -21,11 +23,58 @@ Options for init:
21
23
  --menu-path <path> Default menu path. Default: /MyScripts
22
24
  --scripts-path <path> Source directory to scan. Default: ./src
23
25
  --out-dir <path> Build output directory. Default: ./out
26
+ --app-data-path <path> AppData namespace used by launcher fallbacks. Example: Author/Product
24
27
  --force Overwrite generated files
25
28
  --help Show this message
26
29
  `);
27
30
  }
28
31
 
32
+ function askQuestion(rl, question) {
33
+ return new Promise((resolve) => {
34
+ rl.question(question, (answer) => resolve(answer));
35
+ });
36
+ }
37
+
38
+ async function resolveInitOptions(workdir, options) {
39
+ if (options.appDataPath) {
40
+ return options;
41
+ }
42
+
43
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
44
+ return options;
45
+ }
46
+
47
+ const defaultProductName = path.basename(workdir);
48
+ const rl = readline.createInterface({
49
+ input: process.stdin,
50
+ output: process.stdout,
51
+ });
52
+
53
+ try {
54
+ let author = '';
55
+ while (!author) {
56
+ const answer = await askQuestion(
57
+ rl,
58
+ 'AppData author namespace (for example Vholf3D): '
59
+ );
60
+ author = answer.trim();
61
+ }
62
+
63
+ const productAnswer = await askQuestion(
64
+ rl,
65
+ `AppData product namespace [${defaultProductName}]: `
66
+ );
67
+ const product = productAnswer.trim() || defaultProductName;
68
+
69
+ return {
70
+ ...options,
71
+ appDataPath: `${author}/${product}`,
72
+ };
73
+ } finally {
74
+ rl.close();
75
+ }
76
+ }
77
+
29
78
  function parseOptions(args, defaults) {
30
79
  const options = { ...defaults };
31
80
 
@@ -55,6 +104,12 @@ function parseOptions(args, defaults) {
55
104
  continue;
56
105
  }
57
106
 
107
+ if (arg === '--app-data-path') {
108
+ options.appDataPath = args[index + 1];
109
+ index += 1;
110
+ continue;
111
+ }
112
+
58
113
  if (arg === '--file') {
59
114
  options.file = args[index + 1];
60
115
  index += 1;
@@ -101,6 +156,7 @@ async function main(argv) {
101
156
  menuPath: undefined,
102
157
  scriptsPath: undefined,
103
158
  outDir: undefined,
159
+ appDataPath: undefined,
104
160
  file: undefined,
105
161
  });
106
162
 
@@ -109,7 +165,14 @@ async function main(argv) {
109
165
  return;
110
166
  }
111
167
 
168
+ const commandOptions =
169
+ command === 'init'
170
+ ? await resolveInitOptions(workdir, options)
171
+ : options;
112
172
  const resolvedOptions = getResolvedOptions(workdir, options);
173
+ if (commandOptions !== options) {
174
+ Object.assign(resolvedOptions, commandOptions);
175
+ }
113
176
  resolvedOptions.menuPath = resolvedOptions.menuPath || '/MyScripts';
114
177
  resolvedOptions.scriptsPath = resolvedOptions.scriptsPath || './src';
115
178
  resolvedOptions.outDir = resolvedOptions.outDir || './out';
@@ -49,10 +49,20 @@ export default defineConfig({
49
49
  scriptsPath: '${options.scriptsPath}',
50
50
  outDir: '${options.outDir}',
51
51
  defaultMenuPath: '${options.menuPath}',
52
+ appDataPath: '${options.appDataPath}',
53
+ bundleName: '${options.bundleName}',
52
54
  });
53
55
  `;
54
56
  }
55
57
 
58
+ function toBundleName(projectName) {
59
+ return projectName
60
+ .split(/[-_\s]+/)
61
+ .filter(Boolean)
62
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
63
+ .join(' ');
64
+ }
65
+
56
66
  function buildTsconfigContent() {
57
67
  return `{
58
68
  "extends": "./node_modules/dazscript-framework/tsconfig.json",
@@ -94,11 +104,14 @@ function updatePackageJson(workdir, options) {
94
104
  }
95
105
 
96
106
  function initProject(workdir, rawOptions) {
107
+ const projectName = path.basename(workdir);
97
108
  const options = {
98
109
  force: Boolean(rawOptions.force),
99
110
  menuPath: normalizePath(rawOptions.menuPath, '/MyScripts'),
100
111
  scriptsPath: normalizePath(rawOptions.scriptsPath, './src'),
101
112
  outDir: normalizePath(rawOptions.outDir, './out'),
113
+ appDataPath: normalizePath(rawOptions.appDataPath, `YourName/${projectName}`),
114
+ bundleName: toBundleName(projectName),
102
115
  };
103
116
 
104
117
  writeFileIfNeeded(
@@ -2,6 +2,8 @@ const ts = require('typescript');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const glob = require('glob');
5
+ const { loadConfig } = require('./config-loader');
6
+ const { validateAppDataPath } = require('./app-data-path');
5
7
 
6
8
  const nameofActionFunction = 'action';
7
9
 
@@ -19,19 +21,14 @@ function stringOrDefault(str, defaultValue) {
19
21
  return str !== undefined && str !== null && str !== '' ? str : defaultValue;
20
22
  }
21
23
 
22
- function generateInstallerTemplate(data) {
24
+ function generateInstallerTemplate(data, settingsPath, bundleName) {
23
25
  return `
24
- import { installCustomActions as install } from '@dsf/helpers/custom-action-helper';
26
+ import { showSetupCustomActionsDialog as setup } from '@dsf/helpers/custom-action-installer-helper';
25
27
 
26
- install(${data});
27
- `;
28
- }
29
-
30
- function generateUninstallerTemplate(data) {
31
- return `
32
- import { uninstallCustomActions as uninstall } from '@dsf/helpers/custom-action-helper';
33
-
34
- uninstall(${data});
28
+ setup(${data}, ${JSON.stringify({
29
+ settingsPath,
30
+ bundleName,
31
+ })});
35
32
  `;
36
33
  }
37
34
 
@@ -141,7 +138,20 @@ function findTopLevelActionCall(content, filePath) {
141
138
  return null;
142
139
  }
143
140
 
144
- function processScript(filePath, container, defaultMenuPath) {
141
+ function findActionEntryFiles(workdir, options) {
142
+ const scriptsPath = options.scriptsPath.endsWith('/')
143
+ ? options.scriptsPath
144
+ : `${options.scriptsPath}/`;
145
+ const matches = glob.sync(`${scriptsPath}/**/*.dsa.ts`, { cwd: workdir });
146
+
147
+ return matches.filter((filePath) => {
148
+ const absolutePath = path.join(workdir, filePath);
149
+ const content = fs.readFileSync(absolutePath, 'utf-8').toString();
150
+ return !!findTopLevelActionCall(content, absolutePath);
151
+ });
152
+ }
153
+
154
+ function processScript(filePath, container, defaultMenuPath, settingsPath, bundleName) {
145
155
  const fileInfo = path.parse(filePath);
146
156
  const content = fs.readFileSync(filePath, 'utf-8').toString();
147
157
  const actionCall = findTopLevelActionCall(content, filePath);
@@ -195,52 +205,46 @@ function processScript(filePath, container, defaultMenuPath) {
195
205
  container.scripts.push(script);
196
206
 
197
207
  if (decorator.bundle !== undefined) {
198
- let packageInstallerFilePath = `Install.dsa.ts`;
199
- let packageUninstallerFilePath = `Uninstall.dsa.ts`;
208
+ let packageSetupFilePath = `Setup.dsa.ts`;
200
209
 
201
210
  if (decorator.bundle !== true) {
202
- packageInstallerFilePath = `Install ${decorator.bundle}.dsa.ts`;
203
- packageUninstallerFilePath = `Uninstall ${decorator.bundle}.dsa.ts`;
211
+ packageSetupFilePath = `Setup ${decorator.bundle}.dsa.ts`;
204
212
  }
205
213
 
206
214
  let bundleScriptContent = generateInstallerTemplate(
207
- JSON.stringify(container.scripts, null, 4)
215
+ JSON.stringify(container.scripts, null, 4),
216
+ settingsPath,
217
+ bundleName
208
218
  );
209
219
  let bundleScriptFilePath = path.join(
210
220
  path.parse(filePath).dir,
211
- packageInstallerFilePath
212
- );
213
- fs.writeFileSync(bundleScriptFilePath, bundleScriptContent);
214
-
215
- bundleScriptContent = generateUninstallerTemplate(
216
- JSON.stringify(container.scripts, null, 4)
217
- );
218
- bundleScriptFilePath = path.join(
219
- path.parse(filePath).dir,
220
- packageUninstallerFilePath
221
+ packageSetupFilePath
221
222
  );
222
223
  fs.writeFileSync(bundleScriptFilePath, bundleScriptContent);
223
224
  }
224
225
  }
225
226
 
226
- function processScripts(paths, container, defaultMenuPath) {
227
+ function processScripts(paths, container, defaultMenuPath, settingsPath, bundleName) {
227
228
  paths.forEach((filePath) => {
228
229
  console.log(`Processing ${filePath}`);
229
- processScript(filePath, container, defaultMenuPath);
230
+ processScript(filePath, container, defaultMenuPath, settingsPath, bundleName);
230
231
  });
231
232
  }
232
233
 
233
234
  function generateInstallerFiles(workdir, options) {
234
- const scriptsPath = options.scriptsPath.endsWith('/')
235
- ? options.scriptsPath
236
- : `${options.scriptsPath}/`;
237
235
  const defaultMenuPath = options.defaultMenuPath.endsWith('/')
238
236
  ? options.defaultMenuPath
239
237
  : `${options.defaultMenuPath}/`;
238
+ const { config } = loadConfig(workdir);
239
+ const appDataPath = validateAppDataPath(options.appDataPath || config.appDataPath, workdir);
240
+ const settingsPath = `${appDataPath}/Installer`;
241
+ const bundleName = typeof config.bundleName === 'string' && config.bundleName.trim()
242
+ ? config.bundleName.trim()
243
+ : undefined;
240
244
  const container = { scripts: [] };
241
- const matches = glob.sync(`${scriptsPath}/**/*.dsa.ts`, { cwd: workdir });
245
+ const matches = findActionEntryFiles(workdir, options);
242
246
 
243
- processScripts(matches, container, defaultMenuPath);
247
+ processScripts(matches, container, defaultMenuPath, settingsPath, bundleName);
244
248
 
245
249
  container.scripts = container.scripts.sort((a, b) => {
246
250
  const aKey = a.menuPath + a.filePath;
@@ -249,17 +253,16 @@ function generateInstallerFiles(workdir, options) {
249
253
  });
250
254
 
251
255
  const installerScriptContent = generateInstallerTemplate(
252
- JSON.stringify(container.scripts, null, 4)
256
+ JSON.stringify(container.scripts, null, 4),
257
+ settingsPath,
258
+ bundleName
253
259
  );
254
- fs.writeFileSync(path.join(workdir, 'src', 'Install.dsa.ts'), installerScriptContent);
260
+ fs.writeFileSync(path.join(workdir, 'src', 'Setup.dsa.ts'), installerScriptContent);
255
261
 
256
- const uninstallerScriptContent = generateUninstallerTemplate(
257
- JSON.stringify(container.scripts, null, 4)
258
- );
259
- fs.writeFileSync(
260
- path.join(workdir, 'src', 'Uninstall.dsa.ts'),
261
- uninstallerScriptContent
262
- );
262
+ const installPath = path.join(workdir, 'src', 'Install.dsa.ts');
263
+ const uninstallPath = path.join(workdir, 'src', 'Uninstall.dsa.ts');
264
+ if (fs.existsSync(installPath)) fs.unlinkSync(installPath);
265
+ if (fs.existsSync(uninstallPath)) fs.unlinkSync(uninstallPath);
263
266
  }
264
267
 
265
268
  if (require.main === module) {
@@ -267,6 +270,7 @@ if (require.main === module) {
267
270
  const options = {
268
271
  scriptsPath: './src',
269
272
  defaultMenuPath: 'My Scripts',
273
+ appDataPath: undefined,
270
274
  };
271
275
 
272
276
  for (let index = 0; index < args.length; index += 1) {
@@ -284,6 +288,12 @@ if (require.main === module) {
284
288
  continue;
285
289
  }
286
290
 
291
+ if (arg === '--app-data-path') {
292
+ options.appDataPath = args[index + 1];
293
+ index += 1;
294
+ continue;
295
+ }
296
+
287
297
  throw new Error(`Unknown option: ${arg}`);
288
298
  }
289
299
 
@@ -292,4 +302,5 @@ if (require.main === module) {
292
302
 
293
303
  module.exports = {
294
304
  generateInstallerFiles,
305
+ findActionEntryFiles,
295
306
  };
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { findActionEntryFiles } = require('./install-generator');
6
+ const { validateAppDataPath } = require('./app-data-path');
7
+
8
+ function toPosix(filePath) {
9
+ return filePath.replace(/\\/g, '/');
10
+ }
11
+
12
+ function getActionOutputPath(workdir, outDir, sourceFile) {
13
+ const sourceRoot = path.resolve(workdir, 'src');
14
+ const absoluteSourceFile = path.resolve(workdir, sourceFile);
15
+ const relativeSourceFile = toPosix(path.relative(sourceRoot, absoluteSourceFile));
16
+ return relativeSourceFile.replace(/\.ts$/, '');
17
+ }
18
+
19
+ function getImplementationRelativePath(outputRelativePath) {
20
+ const outputDirectory = path.posix.dirname(outputRelativePath);
21
+ const outputBaseName = path.posix.basename(outputRelativePath, '.dsa');
22
+ const implementationDirectory = outputDirectory === '.'
23
+ ? path.posix.join('lib', outputBaseName)
24
+ : path.posix.join(outputDirectory, 'lib', outputBaseName);
25
+
26
+ return path.posix.join(implementationDirectory, 'script.dsa');
27
+ }
28
+
29
+ function makeLauncherSource(implementationRelativePath, appDataImplementationRelativePath) {
30
+ return [
31
+ '// Auto-generated launcher shim.',
32
+ '// The installed Daz action points at this stable file.',
33
+ '// The current implementation is resolved from the sibling lib/ directory first,',
34
+ '// and from the AppData fallback location second.',
35
+ '',
36
+ `var implementationRelativePath = '${implementationRelativePath}';`,
37
+ `var appDataImplementationRelativePath = '${appDataImplementationRelativePath}';`,
38
+ 'var launcherFileName = getScriptFileName();',
39
+ 'var launcherInfo = new DzFileInfo(launcherFileName);',
40
+ 'var launcherDirectory = typeof launcherInfo.canonicalPath == "function"',
41
+ ' ? launcherInfo.canonicalPath()',
42
+ ' : launcherInfo.path();',
43
+ 'launcherInfo.deleteLater();',
44
+ '',
45
+ "var implementationPath = launcherDirectory + '/' + implementationRelativePath;",
46
+ 'var implementationFile = new DzFile(implementationPath);',
47
+ 'if (!implementationFile.exists()) {',
48
+ ' implementationFile.deleteLater();',
49
+ " implementationPath = App.getAppDataPath() + '/' + appDataImplementationRelativePath;",
50
+ ' implementationFile = new DzFile(implementationPath);',
51
+ '}',
52
+ '',
53
+ 'if (!implementationFile.exists()) {',
54
+ ` MessageBox.warning('Unable to find script implementation:\\n' + implementationPath, 'Missing Script', '&OK;', '');`,
55
+ ' implementationFile.deleteLater();',
56
+ '} else {',
57
+ ' implementationFile.deleteLater();',
58
+ '',
59
+ ' var script = new DzScript(implementationPath);',
60
+ ' if (!script.loadFromFile(implementationPath, true)) {',
61
+ ` MessageBox.warning('Unable to load script implementation:\\n' + implementationPath, 'Script Load Error', '&OK;', '');`,
62
+ ' script.deleteLater();',
63
+ ' } else {',
64
+ " var args = typeof getArguments == 'function' ? getArguments() : [];",
65
+ ' script.execute(args);',
66
+ ' script.deleteLater();',
67
+ ' }',
68
+ '}',
69
+ '',
70
+ ].join('\n');
71
+ }
72
+
73
+ function ensureParentDir(filePath) {
74
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
75
+ }
76
+
77
+ function createActionLaunchers(workdir, options) {
78
+ const outDir = path.resolve(workdir, options.outDir || './out');
79
+ const appDataPath = validateAppDataPath(options.appDataPath, workdir);
80
+ const actionEntryFiles = findActionEntryFiles(workdir, {
81
+ scriptsPath: options.scriptsPath || './src',
82
+ });
83
+
84
+ actionEntryFiles.forEach((sourceFile) => {
85
+ const outputRelativePath = getActionOutputPath(workdir, outDir, sourceFile);
86
+ const launcherPath = path.join(outDir, outputRelativePath);
87
+ const implementationRelativePath = getImplementationRelativePath(outputRelativePath);
88
+ const implementationPath = path.join(outDir, implementationRelativePath);
89
+
90
+ if (!fs.existsSync(launcherPath)) {
91
+ return;
92
+ }
93
+
94
+ ensureParentDir(implementationPath);
95
+ if (fs.existsSync(implementationPath)) {
96
+ fs.unlinkSync(implementationPath);
97
+ }
98
+
99
+ fs.renameSync(launcherPath, implementationPath);
100
+
101
+ const launcherDir = path.dirname(outputRelativePath);
102
+ const relativeImplementationPath = toPosix(
103
+ path.relative(launcherDir || '.', implementationRelativePath)
104
+ );
105
+ const appDataImplementationRelativePath = toPosix(
106
+ path.posix.join(appDataPath, implementationRelativePath)
107
+ );
108
+
109
+ fs.writeFileSync(
110
+ launcherPath,
111
+ makeLauncherSource(
112
+ relativeImplementationPath,
113
+ appDataImplementationRelativePath
114
+ )
115
+ );
116
+ });
117
+ }
118
+
119
+ module.exports = {
120
+ createActionLaunchers,
121
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dazscript-framework",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "author": "Freddy Diaz",
5
5
  "license": "MPL-2.0",
6
6
  "description": "",
@@ -67,6 +67,7 @@
67
67
  },
68
68
  "devDependencies": {
69
69
  "@types/node": "^22.10.5",
70
+ "dazscript-types": "^0.2.4",
70
71
  "eslint": "^9.17.0",
71
72
  "typescript": "^5.7.3"
72
73
  }
@@ -1,7 +1,7 @@
1
1
 
2
- import { installCustomActions as install } from '@dsf/helpers/custom-action-helper';
2
+ import { showSetupCustomActionsDialog as setup } from '@dsf/helpers/custom-action-installer-helper';
3
3
 
4
- install([
4
+ setup([
5
5
  {
6
6
  "name": null,
7
7
  "text": "Hello World",
@@ -16,4 +16,4 @@ install([
16
16
  "menuPath": "/DazScriptFramework/samples",
17
17
  "description": "sample-dialog"
18
18
  }
19
- ]);
19
+ ], {"settingsPath":"DazScriptFramework/samples/Installer","bundleName":"Samples"});