dazscript-framework 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@ The **DazScript Framework** is a TypeScript-based framework for writing Daz Stud
13
13
  ## Features
14
14
 
15
15
  - TypeScript support with full IntelliSense.
16
- - A powerful set of decorators and helper methods for building interactive scripts.
16
+ - A lightweight `action(...)` entrypoint plus helper methods for building interactive scripts.
17
17
  - Easy integration with Daz Studio for quick script deployment.
18
18
 
19
19
  ## Installation
@@ -32,6 +32,8 @@ After installing the package, scaffold the project files:
32
32
  npx dazscript init
33
33
  ```
34
34
 
35
+ 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.
36
+
35
37
  This generates:
36
38
 
37
39
  - `dazscript.config.ts`
@@ -43,15 +45,36 @@ The generated package scripts use the framework CLI directly, so consumer projec
43
45
  You can customize the generated defaults:
44
46
 
45
47
  ```bash
46
- npx dazscript init --menu-path /MyScripts --scripts-path ./src --out-dir ./out
48
+ npx dazscript init --menu-path /MyScripts --scripts-path ./src --out-dir ./out --app-data-path YourName/my-project
47
49
  ```
48
50
 
49
51
  - `--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
52
  - `--scripts-path` tells the installer generator where to scan for runnable `.dsa.ts` entry files.
51
53
  - `--out-dir` sets the webpack build output directory for generated `.dsa` files and copied icons.
54
+ - `--app-data-path` sets the AppData namespace used by launcher fallback resolution. Use a unique `Author/Product` path.
52
55
 
53
56
  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
57
 
58
+ Set `appDataPath` explicitly in `dazscript.config.ts` for every project. It is required for builds that generate launcher shims:
59
+
60
+ ```typescript
61
+ import { defineConfig } from 'dazscript-framework/config';
62
+
63
+ export default defineConfig({
64
+ scriptsPath: './src',
65
+ outDir: './out',
66
+ defaultMenuPath: '/MyScripts',
67
+ appDataPath: 'YourName/my-project',
68
+ });
69
+ ```
70
+
71
+ Built action outputs now use stable launcher shims by default:
72
+
73
+ - `out/<script>.dsa` is the stable launcher registered with Daz Studio menus, toolbars, and shortcuts
74
+ - `out/<folder>/lib/<script-name>/script.dsa` is the current implementation bundle that the launcher executes
75
+
76
+ 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.
77
+
55
78
  ## Usage
56
79
 
57
80
  ### Quick Start: Hello World
@@ -108,6 +131,15 @@ Common `action(...)` parameters:
108
131
  - `description`: a longer description for the action.
109
132
  - `bundle`: generates installer and uninstaller entries as a package bundle instead of a single action entry.
110
133
 
134
+ When an action is built, the framework emits two files for it:
135
+
136
+ - the stable launcher at the original output path
137
+ - the implementation bundle under a sibling `lib/<script-name>/script.dsa` path
138
+
139
+ Generated installers register the launcher path, so menu placement, toolbars, shortcuts, and icons keep pointing at a stable target across rebuilds.
140
+
141
+ 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.
142
+
111
143
  ### Building UIs with Observables & Dialogs
112
144
 
113
145
  The framework uses a **Model-View pattern** with reactive data bindings:
@@ -302,7 +334,7 @@ my-daz-scripts/
302
334
  │ │ ├── my-dialog.ts
303
335
  │ │ └── my-dialog-script.dsa.ts
304
336
  │ └── config.ts
305
- ├── out/ # Generated .dsa files (build output)
337
+ ├── out/ # Generated launchers, implementations, and copied icons
306
338
  ├── package.json
307
339
  ├── tsconfig.json
308
340
  └── dazscript.config.ts
@@ -311,8 +343,10 @@ my-daz-scripts/
311
343
  **Key points:**
312
344
  - Scripts ending in `.dsa.ts` compile to `.dsa` files for Daz Studio
313
345
  - Regular `.ts` files are utility, model, or helper classes
346
+ - Built action outputs are split into stable launchers plus sibling `lib/<script-name>/script.dsa` implementations
314
347
  - Run `npm run build` to compile TypeScript → Daz Scripts
315
348
  - Run `npm run watch` during development for live rebuild
349
+ - Rebuild after script changes; reinstalling Daz actions is usually not required because the launcher path stays stable
316
350
 
317
351
  ## Development & Publishing
318
352
 
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ const path = require('path');
5
+ const readline = require('readline');
4
6
  const { runWebpack } = require('./build');
5
7
  const { loadConfig } = require('./config-loader');
6
8
  const { copyIcons } = require('./icons');
@@ -21,11 +23,58 @@ Options for init:
21
23
  --menu-path <path> Default menu path. Default: /MyScripts
22
24
  --scripts-path <path> Source directory to scan. Default: ./src
23
25
  --out-dir <path> Build output directory. Default: ./out
26
+ --app-data-path <path> AppData namespace used by launcher fallbacks. Example: Author/Product
24
27
  --force Overwrite generated files
25
28
  --help Show this message
26
29
  `);
27
30
  }
28
31
 
32
+ function askQuestion(rl, question) {
33
+ return new Promise((resolve) => {
34
+ rl.question(question, (answer) => resolve(answer));
35
+ });
36
+ }
37
+
38
+ async function resolveInitOptions(workdir, options) {
39
+ if (options.appDataPath) {
40
+ return options;
41
+ }
42
+
43
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
44
+ return options;
45
+ }
46
+
47
+ const defaultProductName = path.basename(workdir);
48
+ const rl = readline.createInterface({
49
+ input: process.stdin,
50
+ output: process.stdout,
51
+ });
52
+
53
+ try {
54
+ let author = '';
55
+ while (!author) {
56
+ const answer = await askQuestion(
57
+ rl,
58
+ 'AppData author namespace (for example Vholf3D): '
59
+ );
60
+ author = answer.trim();
61
+ }
62
+
63
+ const productAnswer = await askQuestion(
64
+ rl,
65
+ `AppData product namespace [${defaultProductName}]: `
66
+ );
67
+ const product = productAnswer.trim() || defaultProductName;
68
+
69
+ return {
70
+ ...options,
71
+ appDataPath: `${author}/${product}`,
72
+ };
73
+ } finally {
74
+ rl.close();
75
+ }
76
+ }
77
+
29
78
  function parseOptions(args, defaults) {
30
79
  const options = { ...defaults };
31
80
 
@@ -55,6 +104,12 @@ function parseOptions(args, defaults) {
55
104
  continue;
56
105
  }
57
106
 
107
+ if (arg === '--app-data-path') {
108
+ options.appDataPath = args[index + 1];
109
+ index += 1;
110
+ continue;
111
+ }
112
+
58
113
  if (arg === '--file') {
59
114
  options.file = args[index + 1];
60
115
  index += 1;
@@ -101,6 +156,7 @@ async function main(argv) {
101
156
  menuPath: undefined,
102
157
  scriptsPath: undefined,
103
158
  outDir: undefined,
159
+ appDataPath: undefined,
104
160
  file: undefined,
105
161
  });
106
162
 
@@ -109,7 +165,14 @@ async function main(argv) {
109
165
  return;
110
166
  }
111
167
 
168
+ const commandOptions =
169
+ command === 'init'
170
+ ? await resolveInitOptions(workdir, options)
171
+ : options;
112
172
  const resolvedOptions = getResolvedOptions(workdir, options);
173
+ if (commandOptions !== options) {
174
+ Object.assign(resolvedOptions, commandOptions);
175
+ }
113
176
  resolvedOptions.menuPath = resolvedOptions.menuPath || '/MyScripts';
114
177
  resolvedOptions.scriptsPath = resolvedOptions.scriptsPath || './src';
115
178
  resolvedOptions.outDir = resolvedOptions.outDir || './out';
@@ -49,6 +49,7 @@ export default defineConfig({
49
49
  scriptsPath: '${options.scriptsPath}',
50
50
  outDir: '${options.outDir}',
51
51
  defaultMenuPath: '${options.menuPath}',
52
+ appDataPath: '${options.appDataPath}',
52
53
  });
53
54
  `;
54
55
  }
@@ -94,11 +95,13 @@ function updatePackageJson(workdir, options) {
94
95
  }
95
96
 
96
97
  function initProject(workdir, rawOptions) {
98
+ const projectName = path.basename(workdir);
97
99
  const options = {
98
100
  force: Boolean(rawOptions.force),
99
101
  menuPath: normalizePath(rawOptions.menuPath, '/MyScripts'),
100
102
  scriptsPath: normalizePath(rawOptions.scriptsPath, './src'),
101
103
  outDir: normalizePath(rawOptions.outDir, './out'),
104
+ appDataPath: normalizePath(rawOptions.appDataPath, `YourName/${projectName}`),
102
105
  };
103
106
 
104
107
  writeFileIfNeeded(
@@ -141,6 +141,19 @@ function findTopLevelActionCall(content, filePath) {
141
141
  return null;
142
142
  }
143
143
 
144
+ function findActionEntryFiles(workdir, options) {
145
+ const scriptsPath = options.scriptsPath.endsWith('/')
146
+ ? options.scriptsPath
147
+ : `${options.scriptsPath}/`;
148
+ const matches = glob.sync(`${scriptsPath}/**/*.dsa.ts`, { cwd: workdir });
149
+
150
+ return matches.filter((filePath) => {
151
+ const absolutePath = path.join(workdir, filePath);
152
+ const content = fs.readFileSync(absolutePath, 'utf-8').toString();
153
+ return !!findTopLevelActionCall(content, absolutePath);
154
+ });
155
+ }
156
+
144
157
  function processScript(filePath, container, defaultMenuPath) {
145
158
  const fileInfo = path.parse(filePath);
146
159
  const content = fs.readFileSync(filePath, 'utf-8').toString();
@@ -231,14 +244,11 @@ function processScripts(paths, container, defaultMenuPath) {
231
244
  }
232
245
 
233
246
  function generateInstallerFiles(workdir, options) {
234
- const scriptsPath = options.scriptsPath.endsWith('/')
235
- ? options.scriptsPath
236
- : `${options.scriptsPath}/`;
237
247
  const defaultMenuPath = options.defaultMenuPath.endsWith('/')
238
248
  ? options.defaultMenuPath
239
249
  : `${options.defaultMenuPath}/`;
240
250
  const container = { scripts: [] };
241
- const matches = glob.sync(`${scriptsPath}/**/*.dsa.ts`, { cwd: workdir });
251
+ const matches = findActionEntryFiles(workdir, options);
242
252
 
243
253
  processScripts(matches, container, defaultMenuPath);
244
254
 
@@ -292,4 +302,5 @@ if (require.main === module) {
292
302
 
293
303
  module.exports = {
294
304
  generateInstallerFiles,
305
+ findActionEntryFiles,
295
306
  };
@@ -0,0 +1,162 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { findActionEntryFiles } = require('./install-generator');
6
+
7
+ function toPosix(filePath) {
8
+ return filePath.replace(/\\/g, '/');
9
+ }
10
+
11
+ function getActionOutputPath(workdir, outDir, sourceFile) {
12
+ const sourceRoot = path.resolve(workdir, 'src');
13
+ const absoluteSourceFile = path.resolve(workdir, sourceFile);
14
+ const relativeSourceFile = toPosix(path.relative(sourceRoot, absoluteSourceFile));
15
+ return relativeSourceFile.replace(/\.ts$/, '');
16
+ }
17
+
18
+ function getImplementationRelativePath(outputRelativePath) {
19
+ const outputDirectory = path.posix.dirname(outputRelativePath);
20
+ const outputBaseName = path.posix.basename(outputRelativePath, '.dsa');
21
+ const implementationDirectory = outputDirectory === '.'
22
+ ? path.posix.join('lib', outputBaseName)
23
+ : path.posix.join(outputDirectory, 'lib', outputBaseName);
24
+
25
+ return path.posix.join(implementationDirectory, 'script.dsa');
26
+ }
27
+
28
+ function validateAppDataPath(appDataPath, workdir) {
29
+ if (!appDataPath || typeof appDataPath !== 'string') {
30
+ throw new Error(
31
+ `[dazscript] Missing required appDataPath in ${workdir}. ` +
32
+ `Set appDataPath: 'Author/Product' in dazscript.config.ts.`
33
+ );
34
+ }
35
+
36
+ const normalized = toPosix(appDataPath).trim().replace(/^\/+|\/+$/g, '');
37
+ const segments = normalized.split('/').filter(Boolean);
38
+
39
+ if (segments.length < 2) {
40
+ throw new Error(
41
+ `[dazscript] Invalid appDataPath "${appDataPath}" in ${workdir}. ` +
42
+ `Use at least two segments, for example 'Author/Product'.`
43
+ );
44
+ }
45
+
46
+ const blockedSegments = new Set([
47
+ 'appdata',
48
+ 'cache',
49
+ 'data',
50
+ 'lib',
51
+ 'libs',
52
+ 'script',
53
+ 'scripts',
54
+ 'temp',
55
+ 'tmp',
56
+ ]);
57
+
58
+ const invalidSegment = segments.find((segment) => blockedSegments.has(segment.toLowerCase()));
59
+ if (invalidSegment) {
60
+ throw new Error(
61
+ `[dazscript] Invalid appDataPath "${appDataPath}" in ${workdir}. ` +
62
+ `Path segments like "${invalidSegment}" are too generic. Use a unique Author/Product path.`
63
+ );
64
+ }
65
+
66
+ return normalized;
67
+ }
68
+
69
+ function makeLauncherSource(implementationRelativePath, appDataImplementationRelativePath) {
70
+ return [
71
+ '// Auto-generated launcher shim.',
72
+ '// The installed Daz action points at this stable file.',
73
+ '// The current implementation is resolved from the sibling lib/ directory first,',
74
+ '// and from the AppData fallback location second.',
75
+ '',
76
+ `var implementationRelativePath = '${implementationRelativePath}';`,
77
+ `var appDataImplementationRelativePath = '${appDataImplementationRelativePath}';`,
78
+ 'var launcherFileName = getScriptFileName();',
79
+ 'var launcherInfo = new DzFileInfo(launcherFileName);',
80
+ 'var launcherDirectory = typeof launcherInfo.canonicalPath == "function"',
81
+ ' ? launcherInfo.canonicalPath()',
82
+ ' : launcherInfo.path();',
83
+ 'launcherInfo.deleteLater();',
84
+ '',
85
+ "var implementationPath = launcherDirectory + '/' + implementationRelativePath;",
86
+ 'var implementationFile = new DzFile(implementationPath);',
87
+ 'if (!implementationFile.exists()) {',
88
+ ' implementationFile.deleteLater();',
89
+ " implementationPath = App.getAppDataPath() + '/' + appDataImplementationRelativePath;",
90
+ ' implementationFile = new DzFile(implementationPath);',
91
+ '}',
92
+ '',
93
+ 'if (!implementationFile.exists()) {',
94
+ ` MessageBox.warning('Unable to find script implementation:\\n' + implementationPath, 'Missing Script', '&OK;', '');`,
95
+ ' implementationFile.deleteLater();',
96
+ '} else {',
97
+ ' implementationFile.deleteLater();',
98
+ '',
99
+ ' var script = new DzScript(implementationPath);',
100
+ ' if (!script.loadFromFile(implementationPath, true)) {',
101
+ ` MessageBox.warning('Unable to load script implementation:\\n' + implementationPath, 'Script Load Error', '&OK;', '');`,
102
+ ' script.deleteLater();',
103
+ ' } else {',
104
+ " var args = typeof getArguments == 'function' ? getArguments() : [];",
105
+ ' script.execute(args);',
106
+ ' script.deleteLater();',
107
+ ' }',
108
+ '}',
109
+ '',
110
+ ].join('\n');
111
+ }
112
+
113
+ function ensureParentDir(filePath) {
114
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
115
+ }
116
+
117
+ function createActionLaunchers(workdir, options) {
118
+ const outDir = path.resolve(workdir, options.outDir || './out');
119
+ const appDataPath = validateAppDataPath(options.appDataPath, workdir);
120
+ const actionEntryFiles = findActionEntryFiles(workdir, {
121
+ scriptsPath: options.scriptsPath || './src',
122
+ });
123
+
124
+ actionEntryFiles.forEach((sourceFile) => {
125
+ const outputRelativePath = getActionOutputPath(workdir, outDir, sourceFile);
126
+ const launcherPath = path.join(outDir, outputRelativePath);
127
+ const implementationRelativePath = getImplementationRelativePath(outputRelativePath);
128
+ const implementationPath = path.join(outDir, implementationRelativePath);
129
+
130
+ if (!fs.existsSync(launcherPath)) {
131
+ return;
132
+ }
133
+
134
+ ensureParentDir(implementationPath);
135
+ if (fs.existsSync(implementationPath)) {
136
+ fs.unlinkSync(implementationPath);
137
+ }
138
+
139
+ fs.renameSync(launcherPath, implementationPath);
140
+
141
+ const launcherDir = path.dirname(outputRelativePath);
142
+ const relativeImplementationPath = toPosix(
143
+ path.relative(launcherDir || '.', implementationRelativePath)
144
+ );
145
+ const appDataImplementationRelativePath = toPosix(
146
+ path.posix.join(appDataPath, implementationRelativePath)
147
+ );
148
+
149
+ fs.writeFileSync(
150
+ launcherPath,
151
+ makeLauncherSource(
152
+ relativeImplementationPath,
153
+ appDataImplementationRelativePath
154
+ )
155
+ );
156
+ });
157
+ }
158
+
159
+ module.exports = {
160
+ createActionLaunchers,
161
+ validateAppDataPath,
162
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dazscript-framework",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "author": "Freddy Diaz",
5
5
  "license": "MPL-2.0",
6
6
  "description": "",
package/webpack.config.js CHANGED
@@ -1,10 +1,33 @@
1
1
  const path = require('path');
2
2
  const glob = require('glob');
3
3
  const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
4
+ const { createActionLaunchers, validateAppDataPath } = require('./dist/scripts/launchers');
5
+ const { loadConfig } = require('./dist/scripts/config-loader');
6
+
7
+ class ActionLauncherPlugin {
8
+ constructor(options) {
9
+ this.options = options;
10
+ }
11
+
12
+ apply(compiler) {
13
+ compiler.hooks.done.tap('ActionLauncherPlugin', () => {
14
+ createActionLaunchers(this.options.workdir, {
15
+ outDir: this.options.outDir,
16
+ scriptsPath: this.options.scriptsPath,
17
+ appDataPath: this.options.appDataPath,
18
+ });
19
+ });
20
+ }
21
+ }
4
22
 
5
23
  module.exports = (env, argv) => {
6
24
  const projectRoot =
7
25
  env && env.context ? path.resolve(env.context) : process.cwd();
26
+ const { config: projectConfig } = loadConfig(projectRoot);
27
+ const appDataPath = validateAppDataPath(
28
+ (env && env.appDataPath) || projectConfig.appDataPath,
29
+ projectRoot
30
+ );
8
31
  const sourceRoot = path.resolve(projectRoot, 'src');
9
32
  const projectNodeModules = path.resolve(projectRoot, 'node_modules');
10
33
  const frameworkSourceRoot = path.resolve(
@@ -88,5 +111,14 @@ module.exports = (env, argv) => {
88
111
  module: false,
89
112
  },
90
113
  },
114
+ plugins: [
115
+ new ActionLauncherPlugin({
116
+ workdir: projectRoot,
117
+ outDir: outputPath,
118
+ scriptsPath:
119
+ (env && env.scriptsPath) || projectConfig.scriptsPath || './src',
120
+ appDataPath,
121
+ }),
122
+ ],
91
123
  };
92
124
  };