dazscript-framework 1.0.4 → 1.0.6
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 +11 -5
- package/dist/scripts/build.js +1 -0
- package/dist/scripts/cli.js +8 -0
- package/dist/scripts/encrypt.js +1 -2
- package/dist/scripts/init.js +3 -0
- package/dist/scripts/launchers.js +66 -0
- package/package.json +1 -1
- package/src/common/log.test.ts +45 -0
- package/src/common/log.ts +62 -14
- package/webpack.config.js +5 -0
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ DAZ Script gives you direct access to the entire Daz Studio API. The DazScript F
|
|
|
32
32
|
- **Fast UI development** — a fluent builder API lets you describe dialogs declaratively without touching the Qt widget API by hand.
|
|
33
33
|
- **Two-way data binding** — link your data model to UI controls so they stay in sync automatically. The user types in a field and your model updates; you update the model in code and the UI reflects it instantly. No manual synchronization needed.
|
|
34
34
|
- **One-command build** — `npm run build` compiles TypeScript to `.dsa` files that Daz Studio runs directly.
|
|
35
|
-
- **Stable launcher shims** — built scripts use a
|
|
35
|
+
- **Stable launcher shims** — built scripts use a launcher/implementation layout so iterating on your code never requires reinstalling actions in Daz Studio.
|
|
36
36
|
- **Automated installer generation** — `npm run installer` produces a full setup dialog by reading action metadata from your source code.
|
|
37
37
|
|
|
38
38
|
---
|
|
@@ -53,7 +53,7 @@ npm install dazscript-framework dazscript-types
|
|
|
53
53
|
npx dazscript init
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
Follow the prompt for your AppData author namespace (e.g. `YourName/my-project`). This creates `dazscript.config.ts`, `tsconfig.json`, and wires the `build`, `watch`, `icons`, and `installer` scripts into `package.json`.
|
|
56
|
+
Follow the prompt for your AppData author namespace (e.g. `YourName/my-project`). This creates `dazscript.config.ts`, `tsconfig.json`, and wires the `build`, `build:encrypted`, `build:release`, `watch`, `encrypt`, `icons`, and `installer` scripts into `package.json`.
|
|
57
57
|
|
|
58
58
|
### 3. Write the script
|
|
59
59
|
|
|
@@ -159,7 +159,8 @@ If `--app-data-path` is not provided, `init` prompts for the AppData author name
|
|
|
159
159
|
This generates:
|
|
160
160
|
- `dazscript.config.ts`
|
|
161
161
|
- `tsconfig.json`
|
|
162
|
-
- `package.json` script wiring for `build`, `watch`, `icons`, and `installer`
|
|
162
|
+
- `package.json` script wiring for `build`, `build:encrypted`, `watch`, `encrypt`, `icons`, and `installer`
|
|
163
|
+
- `build:release` uses `--log-level warn` before encryption so release packages suppress debug and trace output
|
|
163
164
|
|
|
164
165
|
Available `init` flags:
|
|
165
166
|
|
|
@@ -174,6 +175,8 @@ npx dazscript init --menu-path /MyScripts --scripts-path ./src --out-dir ./out -
|
|
|
174
175
|
| `--out-dir` | Where `build` writes `.dsa` files and copies icons |
|
|
175
176
|
| `--app-data-path` | AppData namespace for launcher fallback (`Author/Product` format) |
|
|
176
177
|
|
|
178
|
+
Builds also accept `--log-level <trace|debug|info|warn|error|off>`. This sets the minimum runtime log level for the compiled scripts. Use `debug` or `trace` during development and `warn` for release packages.
|
|
179
|
+
|
|
177
180
|
Use `--scripts-path ./src/scripts` when runnable files live under a subfolder; use `--scripts-path ./src` when they are at the source root.
|
|
178
181
|
|
|
179
182
|
---
|
|
@@ -244,7 +247,7 @@ action({ text: 'My Script' }, MyScript);
|
|
|
244
247
|
Each built action produces two files:
|
|
245
248
|
|
|
246
249
|
- `out/<script>.dsa` — the stable **launcher** registered with Daz Studio (menus, toolbars, shortcuts)
|
|
247
|
-
- `out/<folder>/lib/<script-name
|
|
250
|
+
- `out/<folder>/lib/<script-name>/<script-name>.dsa` — the **implementation bundle** the launcher executes
|
|
248
251
|
|
|
249
252
|
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.
|
|
250
253
|
|
|
@@ -256,7 +259,9 @@ To encrypt implementation bundles before packaging, run the build and then call
|
|
|
256
259
|
npx dazscript encrypt --out-dir ./out --daz-studio "C:/Program Files/DAZ 3D/DAZStudio4/DAZStudio.exe"
|
|
257
260
|
```
|
|
258
261
|
|
|
259
|
-
The `encrypt` command launches Daz Studio with `-headless -noPrompt`, converts each `out/**/lib
|
|
262
|
+
The `encrypt` command launches Daz Studio with `-headless -noPrompt`, converts each implementation bundle under `out/**/lib/**/*.dsa` to a matching `.dse` file, and deletes the source `.dsa` after the matching encrypted file is written. Add `--keep-source` to leave the plain implementation bundles in place.
|
|
263
|
+
|
|
264
|
+
Individual script packages can wrap this command in `package.json` scripts such as `npm run encrypt` or `npm run build:encrypted` so release packaging does not depend on remembering the full Daz Studio executable argument.
|
|
260
265
|
|
|
261
266
|
---
|
|
262
267
|
|
|
@@ -515,6 +520,7 @@ Files ending in `.dsa.ts` are treated as runnable entry points and compiled to `
|
|
|
515
520
|
| Command | What it does |
|
|
516
521
|
|---|---|
|
|
517
522
|
| `npm run build` | Compile TypeScript → Daz Script |
|
|
523
|
+
| `npm run build:release` | Build with `--log-level warn`, then encrypt implementation bundles |
|
|
518
524
|
| `npm run watch` | Recompile on every save |
|
|
519
525
|
| `npm run installer` | Generate the setup dialog |
|
|
520
526
|
| `npm run icons` | Copy icon assets to the output folder |
|
package/dist/scripts/build.js
CHANGED
package/dist/scripts/cli.js
CHANGED
|
@@ -26,6 +26,7 @@ Options:
|
|
|
26
26
|
--scripts-path <path> Source directory to scan. Default: ./src
|
|
27
27
|
--out-dir <path> Build output directory. Default: ./out
|
|
28
28
|
--app-data-path <path> AppData namespace used by launcher fallbacks. Example: Author/Product
|
|
29
|
+
--log-level <level> Minimum runtime log level: trace, debug, info, warn, error, off
|
|
29
30
|
--daz-studio <path> Daz Studio executable for encrypt
|
|
30
31
|
--keep-source Keep source script.dsa files after encrypting
|
|
31
32
|
--timeout-ms <value> Daz Studio encrypt timeout. Default: 300000
|
|
@@ -115,6 +116,12 @@ function parseOptions(args, defaults) {
|
|
|
115
116
|
continue;
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
if (arg === '--log-level') {
|
|
120
|
+
options.logLevel = args[index + 1];
|
|
121
|
+
index += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
118
125
|
if (arg === '--daz-studio') {
|
|
119
126
|
options.dazStudio = args[index + 1];
|
|
120
127
|
index += 1;
|
|
@@ -179,6 +186,7 @@ async function main(argv) {
|
|
|
179
186
|
scriptsPath: undefined,
|
|
180
187
|
outDir: undefined,
|
|
181
188
|
appDataPath: undefined,
|
|
189
|
+
logLevel: undefined,
|
|
182
190
|
file: undefined,
|
|
183
191
|
dazStudio: undefined,
|
|
184
192
|
keepSource: false,
|
package/dist/scripts/encrypt.js
CHANGED
|
@@ -27,8 +27,7 @@ function findImplementationScripts(dir) {
|
|
|
27
27
|
|
|
28
28
|
if (
|
|
29
29
|
entry.isFile() &&
|
|
30
|
-
entry.name
|
|
31
|
-
path.basename(path.dirname(entryPath)) !== 'lib' &&
|
|
30
|
+
entry.name.endsWith('.dsa') &&
|
|
32
31
|
entryPath.split(path.sep).includes('lib')
|
|
33
32
|
) {
|
|
34
33
|
scripts.push(entryPath);
|
package/dist/scripts/init.js
CHANGED
|
@@ -93,8 +93,11 @@ function updatePackageJson(workdir, options) {
|
|
|
93
93
|
...(packageJson.scripts || {}),
|
|
94
94
|
prebuild: 'npm run installer',
|
|
95
95
|
build: 'dazscript build',
|
|
96
|
+
'build:encrypted': 'npm run build && npm run encrypt',
|
|
97
|
+
'build:release': 'npm run build -- --log-level warn && npm run encrypt',
|
|
96
98
|
postbuild: 'npm run icons',
|
|
97
99
|
watch: 'dazscript watch',
|
|
100
|
+
encrypt: 'dazscript encrypt --out-dir ./out --daz-studio "C:\\Program Files\\DAZ 3D\\DAZStudio4\\DAZStudio.exe"',
|
|
98
101
|
icons: 'dazscript icons',
|
|
99
102
|
installer: 'dazscript installer',
|
|
100
103
|
};
|
|
@@ -23,6 +23,26 @@ function getImplementationRelativePath(outputRelativePath, extension = 'dsa') {
|
|
|
23
23
|
? path.posix.join('lib', outputBaseName)
|
|
24
24
|
: path.posix.join(outputDirectory, 'lib', outputBaseName);
|
|
25
25
|
|
|
26
|
+
return path.posix.join(implementationDirectory, `${outputBaseName}.${extension}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getFlatImplementationRelativePath(outputRelativePath, extension = 'dsa') {
|
|
30
|
+
const outputDirectory = path.posix.dirname(outputRelativePath);
|
|
31
|
+
const outputBaseName = path.posix.basename(outputRelativePath, '.dsa');
|
|
32
|
+
const implementationDirectory = outputDirectory === '.'
|
|
33
|
+
? 'lib'
|
|
34
|
+
: path.posix.join(outputDirectory, 'lib');
|
|
35
|
+
|
|
36
|
+
return path.posix.join(implementationDirectory, `${outputBaseName}.${extension}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getLegacyImplementationRelativePath(outputRelativePath, extension = 'dsa') {
|
|
40
|
+
const outputDirectory = path.posix.dirname(outputRelativePath);
|
|
41
|
+
const outputBaseName = path.posix.basename(outputRelativePath, '.dsa');
|
|
42
|
+
const implementationDirectory = outputDirectory === '.'
|
|
43
|
+
? path.posix.join('lib', outputBaseName)
|
|
44
|
+
: path.posix.join(outputDirectory, 'lib', outputBaseName);
|
|
45
|
+
|
|
26
46
|
return path.posix.join(implementationDirectory, `script.${extension}`);
|
|
27
47
|
}
|
|
28
48
|
|
|
@@ -89,6 +109,14 @@ function ensureParentDir(filePath) {
|
|
|
89
109
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
90
110
|
}
|
|
91
111
|
|
|
112
|
+
function removeEmptyDir(dirPath) {
|
|
113
|
+
try {
|
|
114
|
+
fs.rmdirSync(dirPath);
|
|
115
|
+
} catch (_err) {
|
|
116
|
+
// Directory is missing or still contains user/generated files.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
92
120
|
function createActionLaunchers(workdir, options) {
|
|
93
121
|
const outDir = path.resolve(workdir, options.outDir || './out');
|
|
94
122
|
const appDataPath = validateAppDataPath(options.appDataPath, workdir);
|
|
@@ -105,11 +133,34 @@ function createActionLaunchers(workdir, options) {
|
|
|
105
133
|
outDir,
|
|
106
134
|
getImplementationRelativePath(outputRelativePath, 'dse')
|
|
107
135
|
);
|
|
136
|
+
const legacyImplementationPath = path.join(
|
|
137
|
+
outDir,
|
|
138
|
+
getLegacyImplementationRelativePath(outputRelativePath)
|
|
139
|
+
);
|
|
140
|
+
const legacyEncryptedImplementationPath = path.join(
|
|
141
|
+
outDir,
|
|
142
|
+
getLegacyImplementationRelativePath(outputRelativePath, 'dse')
|
|
143
|
+
);
|
|
144
|
+
const flatImplementationPath = path.join(
|
|
145
|
+
outDir,
|
|
146
|
+
getFlatImplementationRelativePath(outputRelativePath)
|
|
147
|
+
);
|
|
148
|
+
const flatEncryptedImplementationPath = path.join(
|
|
149
|
+
outDir,
|
|
150
|
+
getFlatImplementationRelativePath(outputRelativePath, 'dse')
|
|
151
|
+
);
|
|
108
152
|
|
|
109
153
|
if (!fs.existsSync(launcherPath)) {
|
|
110
154
|
return;
|
|
111
155
|
}
|
|
112
156
|
|
|
157
|
+
if (path.resolve(launcherPath) === path.resolve(implementationPath)) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`[dazscript] Launcher path collides with implementation path: ${implementationRelativePath}. ` +
|
|
160
|
+
`Move the source action out of a lib/ folder or use a different script name.`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
113
164
|
ensureParentDir(implementationPath);
|
|
114
165
|
if (fs.existsSync(implementationPath)) {
|
|
115
166
|
fs.unlinkSync(implementationPath);
|
|
@@ -117,6 +168,21 @@ function createActionLaunchers(workdir, options) {
|
|
|
117
168
|
if (fs.existsSync(encryptedImplementationPath)) {
|
|
118
169
|
fs.unlinkSync(encryptedImplementationPath);
|
|
119
170
|
}
|
|
171
|
+
if (fs.existsSync(legacyImplementationPath)) {
|
|
172
|
+
fs.unlinkSync(legacyImplementationPath);
|
|
173
|
+
}
|
|
174
|
+
if (fs.existsSync(legacyEncryptedImplementationPath)) {
|
|
175
|
+
fs.unlinkSync(legacyEncryptedImplementationPath);
|
|
176
|
+
}
|
|
177
|
+
if (fs.existsSync(flatImplementationPath)) {
|
|
178
|
+
fs.unlinkSync(flatImplementationPath);
|
|
179
|
+
}
|
|
180
|
+
if (fs.existsSync(flatEncryptedImplementationPath)) {
|
|
181
|
+
fs.unlinkSync(flatEncryptedImplementationPath);
|
|
182
|
+
}
|
|
183
|
+
if (path.resolve(path.dirname(legacyImplementationPath)) !== path.resolve(path.dirname(implementationPath))) {
|
|
184
|
+
removeEmptyDir(path.dirname(legacyImplementationPath));
|
|
185
|
+
}
|
|
120
186
|
|
|
121
187
|
fs.renameSync(launcherPath, implementationPath);
|
|
122
188
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { debug, error, getLogLevel, info, setLogLevel, shouldLog, trace, warn } from './log'
|
|
3
|
+
|
|
4
|
+
describe('log levels', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
;(globalThis as any).App = {
|
|
7
|
+
debug: vi.fn(),
|
|
8
|
+
log: vi.fn(),
|
|
9
|
+
warning: vi.fn(),
|
|
10
|
+
flushLogBuffer: vi.fn(),
|
|
11
|
+
statusLine: vi.fn()
|
|
12
|
+
}
|
|
13
|
+
setLogLevel('debug')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('filters messages below the active level', () => {
|
|
17
|
+
setLogLevel('warn')
|
|
18
|
+
|
|
19
|
+
debug('hidden debug')
|
|
20
|
+
info('hidden info')
|
|
21
|
+
warn('visible warn')
|
|
22
|
+
error('visible error')
|
|
23
|
+
|
|
24
|
+
expect((globalThis as any).App.debug).not.toHaveBeenCalled()
|
|
25
|
+
expect((globalThis as any).App.log).not.toHaveBeenCalled()
|
|
26
|
+
expect((globalThis as any).App.warning).toHaveBeenCalledTimes(2)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('allows trace only when active level is trace', () => {
|
|
30
|
+
setLogLevel('debug')
|
|
31
|
+
trace('hidden trace')
|
|
32
|
+
|
|
33
|
+
setLogLevel('trace')
|
|
34
|
+
trace('visible trace')
|
|
35
|
+
|
|
36
|
+
expect((globalThis as any).App.debug).toHaveBeenCalledTimes(1)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('normalizes invalid levels to debug', () => {
|
|
40
|
+
setLogLevel('not-a-level')
|
|
41
|
+
|
|
42
|
+
expect(getLogLevel()).toBe('debug')
|
|
43
|
+
expect(shouldLog('debug')).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
})
|
package/src/common/log.ts
CHANGED
|
@@ -1,32 +1,73 @@
|
|
|
1
|
-
|
|
1
|
+
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'off'
|
|
2
|
+
|
|
3
|
+
declare const __DAZSCRIPT_LOG_LEVEL__: string | undefined
|
|
4
|
+
|
|
5
|
+
const logLevels: LogLevel[] = ['trace', 'debug', 'info', 'warn', 'error', 'off']
|
|
6
|
+
const logLevelWeights: { [key in LogLevel]: number } = {
|
|
7
|
+
trace: 0,
|
|
8
|
+
debug: 1,
|
|
9
|
+
info: 2,
|
|
10
|
+
warn: 3,
|
|
11
|
+
error: 4,
|
|
12
|
+
off: 5
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let currentLogLevel: LogLevel = normalizeLogLevel(
|
|
16
|
+
typeof __DAZSCRIPT_LOG_LEVEL__ === 'string'
|
|
17
|
+
? __DAZSCRIPT_LOG_LEVEL__
|
|
18
|
+
: 'debug'
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export const setLogLevel = (level: string): LogLevel => {
|
|
22
|
+
currentLogLevel = normalizeLogLevel(level)
|
|
23
|
+
return currentLogLevel
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const getLogLevel = (): LogLevel => currentLogLevel
|
|
27
|
+
|
|
28
|
+
export function normalizeLogLevel(level: string): LogLevel {
|
|
29
|
+
if (!level) return 'debug'
|
|
30
|
+
const normalized = level.toLowerCase() as LogLevel
|
|
31
|
+
return logLevels.indexOf(normalized) >= 0 ? normalized : 'debug'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const shouldLog = (level: LogLevel): boolean => {
|
|
35
|
+
return logLevelWeights[level] >= logLevelWeights[currentLogLevel]
|
|
36
|
+
&& currentLogLevel !== 'off'
|
|
37
|
+
}
|
|
2
38
|
|
|
3
39
|
export const debug = (message: any) => {
|
|
4
|
-
|
|
5
|
-
|
|
40
|
+
if (!shouldLog('debug')) return
|
|
41
|
+
app().debug(format(message))
|
|
42
|
+
flush()
|
|
6
43
|
}
|
|
7
44
|
|
|
8
45
|
export const info = (message: any) => {
|
|
9
|
-
|
|
10
|
-
|
|
46
|
+
if (!shouldLog('info')) return
|
|
47
|
+
app().log(format(message))
|
|
48
|
+
flush()
|
|
11
49
|
}
|
|
12
50
|
|
|
13
51
|
export const error = (message: any) => {
|
|
14
|
-
|
|
15
|
-
|
|
52
|
+
if (!shouldLog('error')) return
|
|
53
|
+
app().warning(format(message, "ERROR"))
|
|
54
|
+
flush()
|
|
16
55
|
}
|
|
17
56
|
|
|
18
57
|
export const warn = (message: any) => {
|
|
19
|
-
|
|
20
|
-
|
|
58
|
+
if (!shouldLog('warn')) return
|
|
59
|
+
app().warning(message)
|
|
60
|
+
flush()
|
|
21
61
|
}
|
|
22
62
|
|
|
23
63
|
export const trace = (message: any) => {
|
|
24
|
-
|
|
25
|
-
|
|
64
|
+
if (!shouldLog('trace')) return
|
|
65
|
+
app().debug(format(message, "TRACE"))
|
|
66
|
+
flush()
|
|
26
67
|
}
|
|
27
68
|
|
|
28
69
|
export const status = (message: any, log: boolean = true) => {
|
|
29
|
-
|
|
70
|
+
app().statusLine(message, log)
|
|
30
71
|
}
|
|
31
72
|
|
|
32
73
|
/**
|
|
@@ -39,8 +80,9 @@ export const raise = (err: string) => {
|
|
|
39
80
|
}
|
|
40
81
|
|
|
41
82
|
export const dump = (obj: any) => {
|
|
42
|
-
|
|
43
|
-
|
|
83
|
+
if (!shouldLog('debug')) return
|
|
84
|
+
app().debug(`[DUMP]\r\n${JSON.stringify(obj, null, 2)}`)
|
|
85
|
+
flush()
|
|
44
86
|
}
|
|
45
87
|
|
|
46
88
|
const format = (message: string, level?: 'ERROR' | 'TRACE' | 'DUMP' | 'INFO') => {
|
|
@@ -54,3 +96,9 @@ const format = (message: string, level?: 'ERROR' | 'TRACE' | 'DUMP' | 'INFO') =>
|
|
|
54
96
|
return text
|
|
55
97
|
}
|
|
56
98
|
|
|
99
|
+
const app = (): any => App
|
|
100
|
+
|
|
101
|
+
const flush = () => {
|
|
102
|
+
app().flushLogBuffer()
|
|
103
|
+
}
|
|
104
|
+
|
package/webpack.config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const glob = require('glob');
|
|
3
|
+
const webpack = require('webpack');
|
|
3
4
|
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
|
4
5
|
const { createActionLaunchers } = require('./dist/scripts/launchers');
|
|
5
6
|
const { validateAppDataPath } = require('./dist/scripts/app-data-path');
|
|
@@ -29,6 +30,7 @@ module.exports = (env, argv) => {
|
|
|
29
30
|
(env && env.appDataPath) || projectConfig.appDataPath,
|
|
30
31
|
projectRoot
|
|
31
32
|
);
|
|
33
|
+
const logLevel = (env && env.logLevel) || '';
|
|
32
34
|
const sourceRoot = path.resolve(projectRoot, 'src');
|
|
33
35
|
const projectNodeModules = path.resolve(projectRoot, 'node_modules');
|
|
34
36
|
const frameworkSourceRoot = path.resolve(
|
|
@@ -120,6 +122,9 @@ module.exports = (env, argv) => {
|
|
|
120
122
|
},
|
|
121
123
|
},
|
|
122
124
|
plugins: [
|
|
125
|
+
new webpack.DefinePlugin({
|
|
126
|
+
__DAZSCRIPT_LOG_LEVEL__: JSON.stringify(logLevel),
|
|
127
|
+
}),
|
|
123
128
|
new ActionLauncherPlugin({
|
|
124
129
|
workdir: projectRoot,
|
|
125
130
|
outDir: outputPath,
|