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.
@@ -0,0 +1,304 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+
7
+ const defaultTimeoutMs = 300000;
8
+
9
+ function isDazPath(value) {
10
+ return /^[A-Za-z]:[\\/]/.test(String(value || ''));
11
+ }
12
+
13
+ function normalizeForDaz(value) {
14
+ const normalized = String(value).replace(/\\/g, '/');
15
+ const driveMatch = normalized.match(/\/drive_([A-Za-z])\/(.+)$/);
16
+ if (driveMatch) {
17
+ return `${driveMatch[1].toUpperCase()}:/${driveMatch[2]}`;
18
+ }
19
+
20
+ return normalized;
21
+ }
22
+
23
+ function unquoteEnvValue(value) {
24
+ const trimmed = String(value || '').trim();
25
+ if (trimmed.length < 2) return trimmed;
26
+
27
+ const first = trimmed[0];
28
+ const last = trimmed[trimmed.length - 1];
29
+ if ((first === '"' && last === '"') || (first === '\'' && last === '\'')) {
30
+ return trimmed.substring(1, trimmed.length - 1);
31
+ }
32
+
33
+ return trimmed;
34
+ }
35
+
36
+ function loadEnvFile(envPath, targetEnv) {
37
+ if (!envPath || !fs.existsSync(envPath)) return false;
38
+
39
+ const lines = fs.readFileSync(envPath, 'utf8').split(/\r?\n/);
40
+ for (let index = 0; index < lines.length; index += 1) {
41
+ const line = lines[index].trim();
42
+ if (!line || line[0] === '#') continue;
43
+
44
+ const separatorIndex = line.indexOf('=');
45
+ if (separatorIndex <= 0) continue;
46
+
47
+ const key = line.substring(0, separatorIndex).trim();
48
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
49
+ if (targetEnv[key] !== undefined) continue;
50
+
51
+ targetEnv[key] = unquoteEnvValue(line.substring(separatorIndex + 1));
52
+ }
53
+
54
+ return true;
55
+ }
56
+
57
+ function assertRequiredFile(label, filePath, allowDazPath) {
58
+ if (!filePath) {
59
+ throw new Error(`missing required env: ${label}`);
60
+ }
61
+
62
+ if (allowDazPath && isDazPath(filePath)) {
63
+ return filePath;
64
+ }
65
+
66
+ const resolvedPath = path.resolve(filePath);
67
+ if (!fs.existsSync(resolvedPath)) {
68
+ throw new Error(`${label} does not exist: ${resolvedPath}`);
69
+ }
70
+
71
+ return resolvedPath;
72
+ }
73
+
74
+ function resolveIntegrationOptions(cwd, rawOptions, env) {
75
+ const options = rawOptions || {};
76
+ const projectRoot = path.resolve(cwd || process.cwd());
77
+ const envFile = path.resolve(projectRoot, options.envFile || '.env.integration.local');
78
+ const fixturePath = options.fixture
79
+ ? path.resolve(projectRoot, options.fixture)
80
+ : '';
81
+
82
+ if (!fixturePath) {
83
+ throw new Error('missing required option: --fixture <path>');
84
+ }
85
+ if (!fs.existsSync(fixturePath)) {
86
+ throw new Error(`fixture file does not exist: ${fixturePath}`);
87
+ }
88
+
89
+ const dazStudioExe = assertRequiredFile(
90
+ 'DAZ_STUDIO_EXE',
91
+ options.dazStudio || env.DAZ_STUDIO_EXE,
92
+ false
93
+ );
94
+
95
+ const rawContentPath = options.contentDuf || env.DAZ_TEST_CONTENT_DUF || '';
96
+ if (options.requireContent) {
97
+ assertRequiredFile('DAZ_TEST_CONTENT_DUF', rawContentPath, true);
98
+ }
99
+ else if (rawContentPath && !isDazPath(rawContentPath) && !fs.existsSync(path.resolve(rawContentPath))) {
100
+ throw new Error(`DAZ_TEST_CONTENT_DUF does not exist: ${path.resolve(rawContentPath)}`);
101
+ }
102
+
103
+ const timeoutMs = Number(options.timeoutMs || env.DAZ_TEST_TIMEOUT_MS || defaultTimeoutMs);
104
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
105
+ throw new Error(`invalid timeout: ${options.timeoutMs || env.DAZ_TEST_TIMEOUT_MS}`);
106
+ }
107
+
108
+ const outDir = path.resolve(projectRoot, options.outDir || './test/integration/out');
109
+ const fixtureName = path.basename(fixturePath).replace(/\.dsa\.ts$/i, '').replace(/\.ts$/i, '');
110
+ const fixtureRoot = path.join(outDir, 'fixture');
111
+ const resultPath = path.join(fixtureRoot, 'result.json');
112
+ const frameworkRoot = path.resolve(__dirname, '..', '..');
113
+
114
+ return {
115
+ projectRoot,
116
+ frameworkRoot,
117
+ envFile,
118
+ fixturePath,
119
+ fixtureName,
120
+ outDir,
121
+ fixtureRoot,
122
+ resultPath,
123
+ dazStudioExe,
124
+ contentPath: rawContentPath ? normalizeForDaz(rawContentPath) : '',
125
+ requireContent: Boolean(options.requireContent),
126
+ timeoutMs,
127
+ appDataPath: options.appDataPath || 'DazScriptFramework/integration-tests',
128
+ bundleName: options.bundleName || 'Integration Test Fixture',
129
+ env,
130
+ };
131
+ }
132
+
133
+ function run(command, args, options) {
134
+ const result = spawnSync(command, args, {
135
+ cwd: options.cwd,
136
+ env: options.env || process.env,
137
+ encoding: 'utf8',
138
+ stdio: options.stdio || 'inherit',
139
+ timeout: options.timeoutMs,
140
+ });
141
+
142
+ if (result.error) {
143
+ if (result.error.code === 'ETIMEDOUT') {
144
+ throw new Error(`${command} timed out after ${options.timeoutMs}ms`);
145
+ }
146
+ throw result.error;
147
+ }
148
+
149
+ if (result.status !== 0) {
150
+ throw new Error(`${command} ${args.join(' ')} exited with code ${result.status}`);
151
+ }
152
+
153
+ return result;
154
+ }
155
+
156
+ function writeFile(filePath, content) {
157
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
158
+ fs.writeFileSync(filePath, content, 'utf8');
159
+ }
160
+
161
+ function buildFixtureProject(options) {
162
+ fs.rmSync(options.fixtureRoot, { recursive: true, force: true });
163
+ fs.mkdirSync(path.join(options.fixtureRoot, 'src'), { recursive: true });
164
+
165
+ writeFile(path.join(options.fixtureRoot, 'package.json'), JSON.stringify({
166
+ private: true,
167
+ scripts: {
168
+ build: 'dazscript build --out-dir ./out',
169
+ },
170
+ dependencies: {
171
+ 'dazscript-framework': `file:${options.frameworkRoot}`,
172
+ 'dazscript-types': '^1.0.1',
173
+ },
174
+ devDependencies: {},
175
+ }, null, 2));
176
+
177
+ writeFile(path.join(options.fixtureRoot, 'dazscript.config.ts'), [
178
+ "import { defineConfig } from 'dazscript-framework/config';",
179
+ '',
180
+ 'export default defineConfig({',
181
+ " scriptsPath: './src',",
182
+ " outDir: './out',",
183
+ " defaultMenuPath: '/DazScriptFramework/Integration',",
184
+ ` appDataPath: '${options.appDataPath}',`,
185
+ ` bundleName: '${options.bundleName}'`,
186
+ '});',
187
+ '',
188
+ ].join('\n'));
189
+
190
+ writeFile(path.join(options.fixtureRoot, 'tsconfig.json'), JSON.stringify({
191
+ compilerOptions: {
192
+ target: 'ES5',
193
+ baseUrl: '.',
194
+ ignoreDeprecations: '5.0',
195
+ lib: ['ES5'],
196
+ experimentalDecorators: true,
197
+ emitDecoratorMetadata: true,
198
+ noLib: false,
199
+ useDefineForClassFields: false,
200
+ module: 'ESNext',
201
+ paths: {
202
+ '@dst/*': ['node_modules/dazscript-types/src/types/*'],
203
+ '@dsf/*': ['node_modules/dazscript-framework/src/*'],
204
+ },
205
+ declaration: false,
206
+ inlineSourceMap: false,
207
+ removeComments: true,
208
+ preserveConstEnums: true,
209
+ esModuleInterop: true,
210
+ forceConsistentCasingInFileNames: true,
211
+ strict: false,
212
+ strictNullChecks: false,
213
+ strictPropertyInitialization: false,
214
+ noImplicitAny: false,
215
+ strictFunctionTypes: true,
216
+ alwaysStrict: true,
217
+ skipLibCheck: true,
218
+ },
219
+ include: [
220
+ 'node_modules/dazscript-types/src/types/**/*',
221
+ 'src/**/*',
222
+ ],
223
+ }, null, 2));
224
+
225
+ fs.copyFileSync(options.fixturePath, path.join(options.fixtureRoot, 'src', `${options.fixtureName}.dsa.ts`));
226
+ run('npm', ['install', '--ignore-scripts'], { cwd: options.fixtureRoot });
227
+ run('npm', ['run', 'build'], { cwd: options.fixtureRoot });
228
+ }
229
+
230
+ function runDazFixture(options) {
231
+ const launcherPath = path.join(options.fixtureRoot, 'out', `${options.fixtureName}.dsa`);
232
+ if (!fs.existsSync(launcherPath)) {
233
+ throw new Error(`generated launcher was not found: ${launcherPath}`);
234
+ }
235
+
236
+ const dazArgs = [
237
+ '-headless',
238
+ '-noPrompt',
239
+ '-scriptArg',
240
+ normalizeForDaz(options.resultPath),
241
+ ];
242
+
243
+ if (options.contentPath) {
244
+ dazArgs.push('-scriptArg', options.contentPath);
245
+ }
246
+
247
+ dazArgs.push('-script', normalizeForDaz(launcherPath));
248
+
249
+ const useWine = process.platform !== 'win32' && /\.exe$/i.test(options.dazStudioExe);
250
+ const command = useWine ? 'wine' : options.dazStudioExe;
251
+ const commandArgs = useWine ? [options.dazStudioExe].concat(dazArgs) : dazArgs;
252
+
253
+ console.log(`[dazscript integration] launching DAZ: ${useWine ? `${command} ${options.dazStudioExe}` : command}`);
254
+ run(command, commandArgs, {
255
+ cwd: options.fixtureRoot,
256
+ env: options.env,
257
+ timeoutMs: options.timeoutMs,
258
+ });
259
+ }
260
+
261
+ function readIntegrationResult(resultPath) {
262
+ if (!fs.existsSync(resultPath)) {
263
+ throw new Error(`DAZ did not write result JSON: ${resultPath}`);
264
+ }
265
+
266
+ let result;
267
+ try {
268
+ result = JSON.parse(fs.readFileSync(resultPath, 'utf8'));
269
+ }
270
+ catch (error) {
271
+ throw new Error(`could not parse result JSON: ${error}`);
272
+ }
273
+
274
+ if (!result || result.ok !== true) {
275
+ const failures = result && result.failures ? result.failures : ['unknown DAZ integration failure'];
276
+ throw new Error(`DAZ integration assertions failed:\n${failures.map((item) => `- ${item}`).join('\n')}\n\nResult: ${resultPath}`);
277
+ }
278
+
279
+ return result;
280
+ }
281
+
282
+ async function runIntegration(rawOptions, injectedEnv, cwd) {
283
+ const env = { ...(injectedEnv || process.env) };
284
+ const projectRoot = path.resolve(cwd || process.cwd());
285
+ const envFile = path.resolve(projectRoot, (rawOptions && rawOptions.envFile) || '.env.integration.local');
286
+ loadEnvFile(envFile, env);
287
+
288
+ const options = resolveIntegrationOptions(projectRoot, rawOptions, env);
289
+ buildFixtureProject(options);
290
+ runDazFixture(options);
291
+ const result = readIntegrationResult(options.resultPath);
292
+ console.log(`[dazscript integration] passed: ${options.resultPath}`);
293
+ return result;
294
+ }
295
+
296
+ module.exports = {
297
+ loadEnvFile,
298
+ normalizeForDaz,
299
+ readIntegrationResult,
300
+ resolveIntegrationOptions,
301
+ buildFixtureProject,
302
+ runDazFixture,
303
+ runIntegration,
304
+ };
@@ -96,7 +96,13 @@ function makeLauncherSource(implementationRelativePath, appDataImplementationRel
96
96
  ` MessageBox.warning('Unable to load script implementation:\\n' + implementationPath, 'Script Load Error', '&OK;', '');`,
97
97
  ' script.deleteLater();',
98
98
  ' } else {',
99
- " var args = typeof getArguments == 'function' ? getArguments() : [];",
99
+ ' var args = [];',
100
+ " if (typeof getArguments == 'function') {",
101
+ ' args = getArguments();',
102
+ ' }',
103
+ ' if (!args || args.length === 0) {',
104
+ ' args = App.scriptArgs || [];',
105
+ ' }',
100
106
  ' script.execute(args);',
101
107
  ' script.deleteLater();',
102
108
  ' }',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dazscript-framework",
3
- "version": "1.0.29",
3
+ "version": "1.0.31",
4
4
  "author": "Freddy Diaz",
5
5
  "license": "MPL-2.0",
6
6
  "description": "",
@@ -13,6 +13,7 @@
13
13
  "icons": "node ./dist/scripts/cli.js icons --out-dir ./out",
14
14
  "installer": "node ./dist/scripts/cli.js installer --scripts-path ./src/examples --menu-path /DazScriptFramework",
15
15
  "test": "vitest run",
16
+ "test:integration": "node ./dist/scripts/cli.js integration --fixture ./test/integration/fixtures/framework-integration.dsa.ts --require-content",
16
17
  "test:watch": "vitest"
17
18
  },
18
19
  "bin": {
@@ -0,0 +1,95 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ const started: Array<{
4
+ info: string
5
+ totalSteps: number
6
+ isCancellable: boolean
7
+ showTimeElapsed: boolean
8
+ }> = []
9
+ const steps: number[] = []
10
+
11
+ const installProgressGlobals = (cancelled = false): void => {
12
+ vi.stubGlobal('startProgress', (info: string, totalSteps: number, isCancellable: boolean, showTimeElapsed: boolean) => {
13
+ started.push({ info, totalSteps, isCancellable, showTimeElapsed })
14
+ })
15
+ vi.stubGlobal('stepProgress', (count = 1) => {
16
+ steps.push(count)
17
+ })
18
+ vi.stubGlobal('finishProgress', vi.fn())
19
+ vi.stubGlobal('progressIsCancelled', () => cancelled)
20
+ vi.stubGlobal('processEvents', vi.fn())
21
+ }
22
+
23
+ beforeEach(() => {
24
+ started.length = 0
25
+ steps.length = 0
26
+ vi.resetModules()
27
+ })
28
+
29
+ afterEach(() => {
30
+ vi.unstubAllGlobals()
31
+ })
32
+
33
+ describe('withProgress', () => {
34
+ it('starts progress, exposes a manual step handle, and finishes after callback returns', async () => {
35
+ installProgressGlobals()
36
+ const { withProgress } = await import('./progress-helper')
37
+
38
+ const result = withProgress('Manual Work', 4, (progress) => {
39
+ progress.step()
40
+ progress.step(2)
41
+ return 'done'
42
+ })
43
+
44
+ expect(result).toBe('done')
45
+ expect(started).toEqual([
46
+ {
47
+ info: 'Manual Work',
48
+ totalSteps: 4,
49
+ isCancellable: true,
50
+ showTimeElapsed: true,
51
+ },
52
+ ])
53
+ expect(steps).toEqual([1, 2])
54
+ expect(finishProgress).toHaveBeenCalledTimes(1)
55
+ })
56
+
57
+ it('finishes progress when the callback throws', async () => {
58
+ installProgressGlobals()
59
+ const { withProgress } = await import('./progress-helper')
60
+
61
+ expect(() => withProgress('Manual Work', 2, () => {
62
+ throw new Error('failed phase')
63
+ })).toThrow('failed phase')
64
+
65
+ expect(finishProgress).toHaveBeenCalledTimes(1)
66
+ })
67
+
68
+ it('reports cancellation consistently with cancellable progress', async () => {
69
+ installProgressGlobals(true)
70
+ const { withProgress } = await import('./progress-helper')
71
+
72
+ let callbackCancelled = false
73
+ withProgress('Manual Work', 2, (progress) => {
74
+ callbackCancelled = progress.isCancelled()
75
+ progress.step()
76
+ })
77
+
78
+ expect(callbackCancelled).toBe(true)
79
+ expect(processEvents).toHaveBeenCalledTimes(1)
80
+ expect(steps).toEqual([])
81
+ expect(finishProgress).toHaveBeenCalledTimes(1)
82
+ })
83
+ })
84
+
85
+ describe('progress', () => {
86
+ it('finishes progress when an item callback stops the loop', async () => {
87
+ installProgressGlobals()
88
+ const { progress } = await import('./progress-helper')
89
+
90
+ progress('Array Work', [1, 2, 3], (item: number) => item !== 2)
91
+
92
+ expect(steps).toEqual([1])
93
+ expect(finishProgress).toHaveBeenCalledTimes(1)
94
+ })
95
+ })
@@ -1,5 +1,29 @@
1
1
  import { status } from '@dsf/common/log';
2
2
 
3
+ export type ProgressOptions = {
4
+ isCancellable?: boolean
5
+ showTimeElapsed?: boolean
6
+ totalSteps?: number
7
+ }
8
+
9
+ export type ProgressHandle = {
10
+ step: (count?: number) => boolean
11
+ isCancelled: () => boolean
12
+ }
13
+
14
+ const defaultProgressOptions = (totalSteps: number, options?: ProgressOptions): Required<ProgressOptions> => ({
15
+ isCancellable: true,
16
+ showTimeElapsed: true,
17
+ totalSteps,
18
+ ...options
19
+ })
20
+
21
+ const reportInfo = (info: string | string[]): string => {
22
+ const infoMessage = Array.isArray(info) ? '' : info;
23
+ if (Array.isArray(info)) info.forEach(line => status(line));
24
+ return infoMessage;
25
+ }
26
+
3
27
  /**
4
28
  * Displays a progress dialog to the user if one is not already being displayed and starts a progress tracking operation.
5
29
  * @param info The string to display in the progress dialog as the current description of the operation.
@@ -10,43 +34,79 @@ import { status } from '@dsf/common/log';
10
34
  * @param totalSteps The number of progress steps for the operation to be complete.
11
35
  */
12
36
  export const progress = <T>(info: string | string[], items: T[], callback: (item: T) => boolean | void,
13
- options?: { isCancellable?: boolean, showTimeElapsed?: boolean, totalSteps?: number }) => {
37
+ options?: ProgressOptions) => {
14
38
 
15
- options = {
16
- isCancellable: true,
17
- showTimeElapsed: true,
18
- totalSteps: items.length,
19
- ...options
20
- };
39
+ const resolvedOptions = defaultProgressOptions(items.length, options);
21
40
 
22
- const infoMessage = Array.isArray(info) ? '' : info;
23
- startProgress(infoMessage, options.totalSteps, options.isCancellable, options.showTimeElapsed);
41
+ const infoMessage = reportInfo(info);
42
+ startProgress(infoMessage, resolvedOptions.totalSteps, resolvedOptions.isCancellable, resolvedOptions.showTimeElapsed);
24
43
 
25
- if (Array.isArray(info)) info.forEach(line => status(line));
44
+ try {
45
+ // const total = Math.max(1, options.totalSteps ?? items.length);
46
+ // let lastQuarter = -1; // -1 so first boundary triggers
47
+
48
+ for (let i = 0; i < items.length; i++) {
49
+ // cancellation
50
+ if (resolvedOptions.isCancellable && progressIsCancelled()) {
51
+ processEvents(); // flush once on cancel
52
+ break;
53
+ }
54
+
55
+ // work
56
+ if (callback && callback(items[i]) === false) break;
26
57
 
27
- // const total = Math.max(1, options.totalSteps ?? items.length);
28
- // let lastQuarter = -1; // -1 so first boundary triggers
58
+ // progress
59
+ stepProgress(1);
29
60
 
30
- for (let i = 0; i < items.length; i++) {
31
- // cancellation
32
- if (options.isCancellable && progressIsCancelled()) {
33
- processEvents(); // flush once on cancel
34
- break;
61
+ // 25/50/75/100 checkpoints
62
+ // const quarter = Math.floor(((i + 1) * 2) / total); // 0..4
63
+ // if (quarter > lastQuarter) {
64
+ // processEvents();
65
+ // lastQuarter = quarter;
66
+ // }
35
67
  }
68
+ }
69
+ finally {
70
+ finishProgress();
71
+ }
72
+ }
36
73
 
37
- // work
38
- if (callback && callback(items[i]) === false) return;
74
+ /**
75
+ * Starts a progress operation for multi-phase work that does not map to a single item array.
76
+ * Uses script-level cleanup for normal returns and catchable script exceptions; host crashes or native aborts can still bypass script cleanup.
77
+ * @param info The string to display in the progress dialog as the current description of the operation.
78
+ * @param totalSteps The number of progress steps for the operation to be complete.
79
+ * @param callback The function to run with a manual progress handle.
80
+ * @param options Progress dialog options consistent with the array-driven progress helper.
81
+ */
82
+ export const withProgress = <T>(info: string | string[], totalSteps: number, callback: (progress: ProgressHandle) => T,
83
+ options?: Omit<ProgressOptions, 'totalSteps'>): T => {
39
84
 
40
- // progress
41
- stepProgress(1);
85
+ const resolvedOptions = defaultProgressOptions(totalSteps, options);
86
+ const infoMessage = reportInfo(info);
87
+ startProgress(infoMessage, resolvedOptions.totalSteps, resolvedOptions.isCancellable, resolvedOptions.showTimeElapsed);
42
88
 
43
- // 25/50/75/100 checkpoints
44
- // const quarter = Math.floor(((i + 1) * 2) / total); // 0..4
45
- // if (quarter > lastQuarter) {
46
- // processEvents();
47
- // lastQuarter = quarter;
48
- // }
89
+ let flushedCancellation = false;
90
+ const handle: ProgressHandle = {
91
+ isCancelled: () => {
92
+ const cancelled = resolvedOptions.isCancellable && Boolean(progressIsCancelled());
93
+ if (cancelled && !flushedCancellation) {
94
+ processEvents();
95
+ flushedCancellation = true;
96
+ }
97
+ return cancelled;
98
+ },
99
+ step: (count: number = 1) => {
100
+ if (handle.isCancelled()) return false;
101
+ stepProgress(count);
102
+ return true;
103
+ }
49
104
  }
50
105
 
51
- finishProgress();
52
- }
106
+ try {
107
+ return callback(handle);
108
+ }
109
+ finally {
110
+ finishProgress();
111
+ }
112
+ }
@@ -17,7 +17,12 @@ export const getScriptPath = (): string => {
17
17
 
18
18
  export const getScriptArguments = (): any[] => {
19
19
  try {
20
- return typeof getArguments === 'function' ? getArguments() : []
20
+ if (typeof getArguments === 'function') {
21
+ const args = getArguments()
22
+ if (args && args.length > 0) return args
23
+ }
24
+
25
+ return App.scriptArgs ?? []
21
26
  }
22
27
  catch (e) {
23
28
  return []