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 +94 -4
- package/dist/scripts/app-data-path.js +50 -0
- package/dist/scripts/cli.js +63 -0
- package/dist/scripts/init.js +13 -0
- package/dist/scripts/install-generator.js +54 -43
- package/dist/scripts/launchers.js +121 -0
- package/package.json +2 -1
- package/src/{Install.dsa.ts → Setup.dsa.ts} +3 -3
- package/src/helpers/action-helper.ts +14 -1
- package/src/helpers/custom-action-helper.ts +375 -84
- package/src/helpers/custom-action-installer-helper.ts +332 -0
- package/src/shared/set-keyboard-shortcut.ts +29 -5
- package/webpack.config.js +33 -0
- package/src/Uninstall.dsa.ts +0 -19
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
|
|
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
|
|
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
|
|
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
|
+
};
|
package/dist/scripts/cli.js
CHANGED
|
@@ -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';
|
package/dist/scripts/init.js
CHANGED
|
@@ -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 {
|
|
26
|
+
import { showSetupCustomActionsDialog as setup } from '@dsf/helpers/custom-action-installer-helper';
|
|
25
27
|
|
|
26
|
-
|
|
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
|
|
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
|
|
199
|
-
let packageUninstallerFilePath = `Uninstall.dsa.ts`;
|
|
208
|
+
let packageSetupFilePath = `Setup.dsa.ts`;
|
|
200
209
|
|
|
201
210
|
if (decorator.bundle !== true) {
|
|
202
|
-
|
|
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
|
-
|
|
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 =
|
|
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', '
|
|
260
|
+
fs.writeFileSync(path.join(workdir, 'src', 'Setup.dsa.ts'), installerScriptContent);
|
|
255
261
|
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
);
|
|
259
|
-
fs.
|
|
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.
|
|
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 {
|
|
2
|
+
import { showSetupCustomActionsDialog as setup } from '@dsf/helpers/custom-action-installer-helper';
|
|
3
3
|
|
|
4
|
-
|
|
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"});
|