dazscript-framework 1.0.29 → 1.0.31

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
@@ -9,6 +9,8 @@
9
9
  - [Quick Start: A Simple Dialog](#quick-start-a-simple-dialog)
10
10
  - [Documentation](#documentation)
11
11
  - [Installation & Setup](#installation--setup)
12
+ - [Unit Tests](#unit-tests)
13
+ - [DAZ Studio Integration Tests](#daz-studio-integration-tests)
12
14
  - [Project Configuration](#project-configuration)
13
15
  - [The `action(...)` Entrypoint](#the-action-entrypoint)
14
16
  - [Build Output: Launcher Shims](#build-output-launcher-shims)
@@ -55,6 +57,13 @@ npx dazscript init
55
57
 
56
58
  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
59
 
60
+ To include optional test scaffolds, run one or both:
61
+
62
+ ```bash
63
+ npx dazscript init --unit-tests
64
+ npx dazscript init --integration-tests
65
+ ```
66
+
58
67
  ### 3. Write the script
59
68
 
60
69
  Create `src/hello-world.dsa.ts`:
@@ -166,6 +175,8 @@ Available `init` flags:
166
175
 
167
176
  ```bash
168
177
  npx dazscript init --menu-path /MyScripts --scripts-path ./src --out-dir ./out --app-data-path YourName/my-project
178
+ npx dazscript init --unit-tests --app-data-path YourName/my-project
179
+ npx dazscript init --integration-tests --app-data-path YourName/my-project
169
180
  ```
170
181
 
171
182
  | Flag | Description |
@@ -174,11 +185,43 @@ npx dazscript init --menu-path /MyScripts --scripts-path ./src --out-dir ./out -
174
185
  | `--scripts-path` | Where the generator scans for runnable `.dsa.ts` entry files |
175
186
  | `--out-dir` | Where `build` writes `.dsa` files and copies icons |
176
187
  | `--app-data-path` | AppData namespace for launcher fallback (`Author/Product` format) |
188
+ | `--unit-tests` | Add Vitest config, sample unit test, unit-test docs, `test` scripts, and the `vitest` dev dependency |
189
+ | `--integration-tests` | Add a DAZ Studio headless integration-test fixture, env examples, ignore entries, and `test:integration` script |
177
190
 
178
191
  Builds also accept `--log-level <trace|debug|info|warn|error|off>`. This sets the minimum runtime log level for the compiled scripts. When omitted, builds default to `debug`; use `warn` for release packages.
179
192
 
180
193
  Use `--scripts-path ./src/scripts` when runnable files live under a subfolder; use `--scripts-path ./src` when they are at the source root.
181
194
 
195
+ ### Unit tests
196
+
197
+ Projects can bootstrap fast Node-side TypeScript tests with:
198
+
199
+ ```bash
200
+ npx dazscript init --unit-tests
201
+ ```
202
+
203
+ This adds:
204
+
205
+ - `vitest.config.ts`
206
+ - `tsconfig.test.json`
207
+ - `test/unit/smoke.test.ts`
208
+ - `test/unit/README.md`
209
+ - `npm test`
210
+ - `npm run test:watch`
211
+ - `vitest` as a dev dependency
212
+
213
+ Use unit tests for pure TypeScript helpers, parsing, normalization, and code that does not need Daz Studio runtime objects.
214
+
215
+ ### DAZ Studio integration tests
216
+
217
+ Projects can run real DAZ Studio smoke tests through the published CLI:
218
+
219
+ ```bash
220
+ npm run test:integration
221
+ ```
222
+
223
+ The generated script calls `dazscript integration --fixture ./test/integration/fixtures/<project>-smoke.dsa.ts`. Configure local machine paths in an ignored `.env.integration.local`; `DAZ_STUDIO_EXE` is always required, and `DAZ_TEST_CONTENT_DUF` is required only for tests that use `--require-content`. See `test/integration/README.md` in this repository for maintainer and consuming-project details.
224
+
182
225
  ---
183
226
 
184
227
  ### Project Configuration
@@ -528,14 +571,29 @@ my-daz-scripts/
528
571
  │ ├── my-dialog-model.ts # plain TypeScript — model or helper class
529
572
  │ ├── my-dialog.ts
530
573
  │ └── my-dialog-script.dsa.ts # runnable entry point
574
+ ├── test/
575
+ │ ├── integration/
576
+ │ │ ├── fixtures/
577
+ │ │ │ └── my-daz-scripts-smoke.dsa.ts
578
+ │ │ ├── out/ # integration output, ignored by git
579
+ │ │ └── README.md
580
+ │ └── unit/
581
+ │ ├── smoke.test.ts
582
+ │ └── README.md
531
583
  ├── out/ # build output — launchers, bundles, icons
584
+ ├── .env.integration.linux.example
585
+ ├── .env.integration.windows.example
532
586
  ├── dazscript.config.ts
533
587
  ├── tsconfig.json
588
+ ├── tsconfig.test.json
589
+ ├── vitest.config.ts
534
590
  └── package.json
535
591
  ```
536
592
 
537
593
  Files ending in `.dsa.ts` are treated as runnable entry points and compiled to `.dsa`. Plain `.ts` files are modules — imported by entry points but not compiled independently.
538
594
 
595
+ The `test/unit/` files are generated only when you run `dazscript init --unit-tests`. The `test/integration/` files and env examples are generated only when you run `dazscript init --integration-tests`.
596
+
539
597
  **Common commands**
540
598
 
541
599
  | Command | What it does |
@@ -545,6 +603,8 @@ Files ending in `.dsa.ts` are treated as runnable entry points and compiled to `
545
603
  | `npm run watch` | Recompile on every save |
546
604
  | `npm run installer` | Generate the setup dialog |
547
605
  | `npm run icons` | Copy icon assets to the output folder |
606
+ | `npm test` | Run fast Node/Vitest tests |
607
+ | `npm run test:integration` | Run a DAZ Studio headless integration fixture |
548
608
 
549
609
  ---
550
610
 
@@ -9,6 +9,7 @@ const { copyIcons } = require('./icons');
9
9
  const { runEncrypt } = require('./encrypt');
10
10
  const { generateInstallerFiles } = require('./install-generator');
11
11
  const { initProject } = require('./init');
12
+ const { runIntegration } = require('./integration');
12
13
 
13
14
  function printHelp() {
14
15
  console.log(`dazscript <command> [options]
@@ -20,6 +21,7 @@ Commands:
20
21
  encrypt Encrypt built implementation bundles through Daz Studio
21
22
  icons Copy png assets into the output directory
22
23
  installer Generate Install.dsa.ts and Uninstall.dsa.ts
24
+ integration Run a DAZ Studio headless integration fixture
23
25
 
24
26
  Options:
25
27
  --menu-path <path> Default menu path. Default: /MyScripts
@@ -30,6 +32,11 @@ Options:
30
32
  --daz-studio <path> Daz Studio executable for encrypt
31
33
  --keep-source Keep source script.dsa files after encrypting
32
34
  --timeout-ms <value> Daz Studio encrypt timeout. Default: 300000
35
+ --fixture <path> Integration fixture .dsa.ts file
36
+ --env-file <path> Integration env file. Default: .env.integration.local
37
+ --require-content Require DAZ_TEST_CONTENT_DUF for integration tests
38
+ --unit-tests Add Vitest unit-test scaffold during init
39
+ --integration-tests Add integration-test scaffold during init
33
40
  --force Overwrite generated files
34
41
  --help Show this message
35
42
  `);
@@ -92,6 +99,16 @@ function parseOptions(args, defaults) {
92
99
  continue;
93
100
  }
94
101
 
102
+ if (arg === '--integration-tests') {
103
+ options.integrationTests = true;
104
+ continue;
105
+ }
106
+
107
+ if (arg === '--unit-tests') {
108
+ options.unitTests = true;
109
+ continue;
110
+ }
111
+
95
112
  if (arg === '--menu-path') {
96
113
  options.menuPath = args[index + 1];
97
114
  index += 1;
@@ -139,6 +156,23 @@ function parseOptions(args, defaults) {
139
156
  continue;
140
157
  }
141
158
 
159
+ if (arg === '--fixture') {
160
+ options.fixture = args[index + 1];
161
+ index += 1;
162
+ continue;
163
+ }
164
+
165
+ if (arg === '--env-file') {
166
+ options.envFile = args[index + 1];
167
+ index += 1;
168
+ continue;
169
+ }
170
+
171
+ if (arg === '--require-content') {
172
+ options.requireContent = true;
173
+ continue;
174
+ }
175
+
142
176
  if (arg === '--file') {
143
177
  options.file = args[index + 1];
144
178
  index += 1;
@@ -191,6 +225,11 @@ async function main(argv) {
191
225
  dazStudio: undefined,
192
226
  keepSource: false,
193
227
  timeoutMs: undefined,
228
+ fixture: undefined,
229
+ envFile: undefined,
230
+ requireContent: false,
231
+ unitTests: false,
232
+ integrationTests: false,
194
233
  });
195
234
 
196
235
  if (options.help) {
@@ -198,6 +237,11 @@ async function main(argv) {
198
237
  return;
199
238
  }
200
239
 
240
+ if (command === 'integration') {
241
+ await runIntegration(options, process.env, workdir);
242
+ return;
243
+ }
244
+
201
245
  const commandOptions =
202
246
  command === 'init'
203
247
  ? await resolveInitOptions(workdir, options)
@@ -37,11 +37,28 @@ function writeFileIfNeeded(filePath, content, force) {
37
37
  return false;
38
38
  }
39
39
 
40
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
40
41
  fs.writeFileSync(filePath, content);
41
42
  console.log(`write ${path.basename(filePath)}`);
42
43
  return true;
43
44
  }
44
45
 
46
+ function ensureLine(filePath, line) {
47
+ const existing = fs.existsSync(filePath)
48
+ ? fs.readFileSync(filePath, 'utf8')
49
+ : '';
50
+ const lines = existing.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
51
+
52
+ if (lines.includes(line)) {
53
+ return false;
54
+ }
55
+
56
+ const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
57
+ fs.appendFileSync(filePath, `${prefix}${line}\n`);
58
+ console.log(`update ${path.basename(filePath)}`);
59
+ return true;
60
+ }
61
+
45
62
  function buildConfigContent(options) {
46
63
  return `import { defineConfig } from 'dazscript-framework/config';
47
64
 
@@ -63,6 +80,13 @@ function toBundleName(projectName) {
63
80
  .join(' ');
64
81
  }
65
82
 
83
+ function toFixtureName(projectName) {
84
+ return projectName
85
+ .toLowerCase()
86
+ .replace(/[^a-z0-9]+/g, '-')
87
+ .replace(/^-+|-+$/g, '') || 'project';
88
+ }
89
+
66
90
  function buildTsconfigContent() {
67
91
  return `{
68
92
  "extends": "./node_modules/dazscript-framework/tsconfig.json",
@@ -106,6 +130,299 @@ function updatePackageJson(workdir, options) {
106
130
  console.log('update package.json');
107
131
  }
108
132
 
133
+ function updatePackageJsonForIntegration(workdir, fixturePath) {
134
+ const packageJsonPath = path.join(workdir, 'package.json');
135
+ const packageJson = fs.existsSync(packageJsonPath)
136
+ ? readJson(packageJsonPath)
137
+ : { private: true };
138
+
139
+ packageJson.scripts = packageJson.scripts || {};
140
+ if (packageJson.scripts['test:integration']) {
141
+ console.log('skip package.json test:integration');
142
+ }
143
+ else {
144
+ packageJson.scripts['test:integration'] = `dazscript integration --fixture ${fixturePath}`;
145
+ writeJson(packageJsonPath, packageJson);
146
+ console.log('update package.json');
147
+ }
148
+ }
149
+
150
+ function isNpmDefaultTestScript(scriptName, command) {
151
+ return scriptName === 'test' && command === 'echo "Error: no test specified" && exit 1';
152
+ }
153
+
154
+ function setPackageScript(packageJson, scriptName, command) {
155
+ packageJson.scripts = packageJson.scripts || {};
156
+ const existingCommand = packageJson.scripts[scriptName];
157
+ if (existingCommand && !isNpmDefaultTestScript(scriptName, existingCommand)) {
158
+ console.log(`skip package.json ${scriptName}`);
159
+ return false;
160
+ }
161
+
162
+ packageJson.scripts[scriptName] = command;
163
+ return true;
164
+ }
165
+
166
+ function setDevDependency(packageJson, dependencyName, version) {
167
+ packageJson.devDependencies = packageJson.devDependencies || {};
168
+ if (packageJson.devDependencies[dependencyName]) {
169
+ return false;
170
+ }
171
+
172
+ packageJson.devDependencies[dependencyName] = version;
173
+ return true;
174
+ }
175
+
176
+ function updatePackageJsonForUnitTests(workdir) {
177
+ const packageJsonPath = path.join(workdir, 'package.json');
178
+ const packageJson = fs.existsSync(packageJsonPath)
179
+ ? readJson(packageJsonPath)
180
+ : { private: true };
181
+
182
+ let changed = false;
183
+ changed = setPackageScript(packageJson, 'test', 'vitest run') || changed;
184
+ changed = setPackageScript(packageJson, 'test:watch', 'vitest') || changed;
185
+ changed = setDevDependency(packageJson, 'vitest', '^3.0.0') || changed;
186
+
187
+ if (changed) {
188
+ writeJson(packageJsonPath, packageJson);
189
+ console.log('update package.json');
190
+ }
191
+ }
192
+
193
+ function buildVitestConfigContent() {
194
+ return `import path from 'node:path'
195
+ import { defineConfig } from 'vitest/config'
196
+
197
+ export default defineConfig({
198
+ test: {
199
+ environment: 'node',
200
+ include: ['src/**/*.test.ts', 'test/unit/**/*.test.ts']
201
+ },
202
+ resolve: {
203
+ alias: {
204
+ '@dsf': path.resolve(__dirname, 'node_modules/dazscript-framework/src'),
205
+ '@dst': path.resolve(__dirname, 'node_modules/dazscript-types/src/types')
206
+ }
207
+ }
208
+ })
209
+ `;
210
+ }
211
+
212
+ function buildTestTsconfigContent() {
213
+ return `{
214
+ "extends": "./tsconfig.json",
215
+ "compilerOptions": {
216
+ "moduleResolution": "Node"
217
+ },
218
+ "include": ["src/**/*.test.ts", "test/unit/**/*.test.ts"],
219
+ "exclude": []
220
+ }
221
+ `;
222
+ }
223
+
224
+ function buildUnitTestContent() {
225
+ return `import { describe, expect, it } from 'vitest'
226
+
227
+ const toTitleCase = (value: string): string =>
228
+ value
229
+ .split(/[-_\\s]+/)
230
+ .filter(Boolean)
231
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
232
+ .join(' ')
233
+
234
+ describe('unit test scaffold', () => {
235
+ it('runs TypeScript tests with Vitest', () => {
236
+ expect(toTitleCase('hello-daz-script')).toBe('Hello Daz Script')
237
+ })
238
+ })
239
+ `;
240
+ }
241
+
242
+ function buildUnitTestReadmeContent() {
243
+ return `# Unit Tests
244
+
245
+ This project uses Vitest for fast Node-side unit tests.
246
+
247
+ Run once:
248
+
249
+ \`\`\`bash
250
+ npm test
251
+ \`\`\`
252
+
253
+ Watch mode:
254
+
255
+ \`\`\`bash
256
+ npm run test:watch
257
+ \`\`\`
258
+
259
+ Unit tests are useful for pure TypeScript helpers, parsing, normalization, and other logic that does not require Daz Studio runtime objects. Use DAZ Studio integration tests for helpers that must execute inside Daz Studio.
260
+ `;
261
+ }
262
+
263
+ function initUnitTests(workdir, options) {
264
+ writeFileIfNeeded(
265
+ path.join(workdir, 'vitest.config.ts'),
266
+ buildVitestConfigContent(),
267
+ options.force
268
+ );
269
+ writeFileIfNeeded(
270
+ path.join(workdir, 'tsconfig.test.json'),
271
+ buildTestTsconfigContent(),
272
+ options.force
273
+ );
274
+ writeFileIfNeeded(
275
+ path.join(workdir, 'test/unit/smoke.test.ts'),
276
+ buildUnitTestContent(),
277
+ options.force
278
+ );
279
+ writeFileIfNeeded(
280
+ path.join(workdir, 'test/unit/README.md'),
281
+ buildUnitTestReadmeContent(),
282
+ options.force
283
+ );
284
+ updatePackageJsonForUnitTests(workdir);
285
+ }
286
+
287
+ function buildIntegrationFixtureContent() {
288
+ return `import { action } from '@dsf/core/action'
289
+ import { saveToFile } from '@dsf/helpers/file-helper'
290
+ import { currentTime, frameToTime, getCurrentFrame, timeToFrame } from '@dsf/helpers/scene-helper'
291
+ import { getStringScriptArguments } from '@dsf/helpers/script-helper'
292
+
293
+ type IntegrationResult = {
294
+ ok: boolean
295
+ project: string
296
+ checks: Record<string, unknown>
297
+ failures: string[]
298
+ }
299
+
300
+ const isNumber = (value: unknown): boolean => typeof value === 'number' && !isNaN(value as number)
301
+
302
+ action({ text: 'Integration Smoke Test', menuPath: false }, () => {
303
+ const args = getStringScriptArguments()
304
+ const resultPath = args.length > 0 ? args[0] : ''
305
+ const failures: string[] = []
306
+ const frame = getCurrentFrame()
307
+ const time = frameToTime(frame)
308
+ const current = currentTime()
309
+ const roundTripFrame = timeToFrame(time)
310
+
311
+ if (!resultPath) {
312
+ failures.push('missing result path argument')
313
+ }
314
+ if (!isNumber(frame)) {
315
+ failures.push('scene-helper getCurrentFrame did not return a number')
316
+ }
317
+ if (current === null || current === undefined) {
318
+ failures.push('scene-helper currentTime returned null or undefined')
319
+ }
320
+ if (roundTripFrame !== frame) {
321
+ failures.push(\`frame/time round trip failed: \${frame} -> \${time} -> \${roundTripFrame}\`)
322
+ }
323
+
324
+ const result: IntegrationResult = {
325
+ ok: failures.length === 0,
326
+ project: 'integration-smoke',
327
+ checks: {
328
+ frame,
329
+ time: String(time),
330
+ currentTime: String(current),
331
+ roundTripFrame
332
+ },
333
+ failures
334
+ }
335
+
336
+ if (resultPath) {
337
+ saveToFile(resultPath, JSON.stringify(result, null, 2))
338
+ }
339
+ })
340
+ `;
341
+ }
342
+
343
+ function buildIntegrationReadmeContent(fixturePath) {
344
+ return `# Integration Tests
345
+
346
+ This project uses the framework DAZ Studio headless integration harness.
347
+
348
+ Create a local env file from the example for your OS:
349
+
350
+ \`\`\`bash
351
+ cp .env.integration.linux.example .env.integration.local
352
+ \`\`\`
353
+
354
+ \`\`\`powershell
355
+ Copy-Item .env.integration.windows.example .env.integration.local
356
+ \`\`\`
357
+
358
+ Then run:
359
+
360
+ \`\`\`bash
361
+ npm run test:integration
362
+ \`\`\`
363
+
364
+ The default smoke fixture is figure-independent:
365
+
366
+ \`\`\`text
367
+ ${fixturePath}
368
+ \`\`\`
369
+
370
+ Generated output is written to \`test/integration/out/\` and ignored by git.
371
+ `;
372
+ }
373
+
374
+ function buildLinuxEnvExample() {
375
+ return `# Copy this file to .env.integration.local and edit paths for your machine.
376
+ # Shell environment variables override values in .env.integration.local.
377
+ # DAZ_STUDIO_EXE is required for all integration tests.
378
+ # DAZ_TEST_CONTENT_DUF is required only when the fixture or npm script uses --require-content.
379
+
380
+ WINEPREFIX=/home/your-user/.local/share/daz-wine/prefix
381
+ DAZ_STUDIO_EXE=/home/your-user/.local/share/daz-wine/prefix/drive_c/Program Files/DAZ 3D/DAZStudio4/DAZStudio.exe
382
+ # DAZ_TEST_CONTENT_DUF=/home/your-user/.local/share/daz-wine/prefix/drive_c/users/Public/Documents/My DAZ 3D Library/People/Genesis 9/Genesis 9.duf
383
+ DAZ_TEST_TIMEOUT_MS=300000
384
+ `;
385
+ }
386
+
387
+ function buildWindowsEnvExample() {
388
+ return `# Copy this file to .env.integration.local and edit paths for your machine.
389
+ # Shell environment variables override values in .env.integration.local.
390
+ # Forward slashes avoid escaping issues in env files.
391
+ # DAZ_STUDIO_EXE is required for all integration tests.
392
+ # DAZ_TEST_CONTENT_DUF is required only when the fixture or npm script uses --require-content.
393
+
394
+ DAZ_STUDIO_EXE=C:/Program Files/DAZ 3D/DAZStudio4/DAZStudio.exe
395
+ # DAZ_TEST_CONTENT_DUF=C:/Users/Public/Documents/My DAZ 3D Library/People/Genesis 9/Genesis 9.duf
396
+ DAZ_TEST_TIMEOUT_MS=300000
397
+ `;
398
+ }
399
+
400
+ function initIntegrationTests(workdir, options) {
401
+ const fixtureBaseName = `${toFixtureName(path.basename(workdir))}-smoke.dsa.ts`;
402
+ const fixturePath = `./test/integration/fixtures/${fixtureBaseName}`;
403
+ const localFixturePath = path.join(workdir, fixturePath);
404
+
405
+ writeFileIfNeeded(localFixturePath, buildIntegrationFixtureContent(), options.force);
406
+ writeFileIfNeeded(
407
+ path.join(workdir, 'test/integration/README.md'),
408
+ buildIntegrationReadmeContent(fixturePath),
409
+ options.force
410
+ );
411
+ writeFileIfNeeded(
412
+ path.join(workdir, '.env.integration.linux.example'),
413
+ buildLinuxEnvExample(),
414
+ options.force
415
+ );
416
+ writeFileIfNeeded(
417
+ path.join(workdir, '.env.integration.windows.example'),
418
+ buildWindowsEnvExample(),
419
+ options.force
420
+ );
421
+ updatePackageJsonForIntegration(workdir, fixturePath);
422
+ ensureLine(path.join(workdir, '.gitignore'), 'test/integration/out/');
423
+ ensureLine(path.join(workdir, '.gitignore'), '.env.integration.local');
424
+ }
425
+
109
426
  function initProject(workdir, rawOptions) {
110
427
  const projectName = path.basename(workdir);
111
428
  const options = {
@@ -115,6 +432,8 @@ function initProject(workdir, rawOptions) {
115
432
  outDir: normalizePath(rawOptions.outDir, './out'),
116
433
  appDataPath: normalizePath(rawOptions.appDataPath, `YourName/${projectName}`),
117
434
  bundleName: toBundleName(projectName),
435
+ unitTests: Boolean(rawOptions.unitTests),
436
+ integrationTests: Boolean(rawOptions.integrationTests),
118
437
  };
119
438
 
120
439
  writeFileIfNeeded(
@@ -129,8 +448,18 @@ function initProject(workdir, rawOptions) {
129
448
  );
130
449
 
131
450
  updatePackageJson(workdir, options);
451
+
452
+ if (options.unitTests) {
453
+ initUnitTests(workdir, options);
454
+ }
455
+
456
+ if (options.integrationTests) {
457
+ initIntegrationTests(workdir, options);
458
+ }
132
459
  }
133
460
 
134
461
  module.exports = {
462
+ initIntegrationTests,
463
+ initUnitTests,
135
464
  initProject,
136
465
  };