dazscript-framework 0.2.3 → 0.2.4

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
@@ -1,6 +1,6 @@
1
1
  # DazScript Framework
2
2
 
3
- > ⚠️ **EARLY VERSION** — This framework is in active development (v0.1.15). The API is not yet stable and may change between releases. Not recommended for production use until v1.0 is released.
3
+ > ⚠️ This framework is under active development. The API may still evolve between releases. If you need a stable long-term surface, wait for `v1.0`.
4
4
 
5
5
  The **DazScript Framework** is a TypeScript-based framework for writing Daz Studio scripts. It provides all the advantages of a typed language such as autocompletion, error checking, and method parameter documentation and hinting. The framework also includes a set of dialog helpers for rapid UI development.
6
6
 
@@ -46,7 +46,7 @@ You can customize the generated defaults:
46
46
  npx dazscript init --menu-path /MyScripts --scripts-path ./src --out-dir ./out
47
47
  ```
48
48
 
49
- - `--menu-path` sets which Daz Studio menu the scripts are added to by default. See [The `@action` Decorator](#the-action-decorator) for how a script can override that with `menuPath`.
49
+ - `--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
50
  - `--scripts-path` tells the installer generator where to scan for runnable `.dsa.ts` entry files.
51
51
  - `--out-dir` sets the webpack build output directory for generated `.dsa` files and copied icons.
52
52
 
@@ -60,42 +60,45 @@ Create a simple script that logs to the console:
60
60
 
61
61
  ```typescript
62
62
  import { debug } from '@dsf/common/log';
63
- import { action } from '@dsf/core/action-decorator';
64
- import { BaseScript } from '@dsf/core/base-script';
63
+ import { action } from '@dsf/core/action';
65
64
  import { info } from '@dsf/helpers/message-box-helper';
66
65
 
67
- @action({ text: 'Hello World' })
68
- class HelloWorldScript extends BaseScript {
69
- protected run(): void {
70
- debug('Hello World!');
71
- info('Hello World!');
72
- }
73
- }
74
-
75
- new HelloWorldScript().exec();
66
+ action({ text: 'Hello World' }, () => {
67
+ debug('Hello World!');
68
+ info('Hello World!');
69
+ });
76
70
  ```
77
71
 
78
- ### The `@action` Decorator
72
+ ### The `action(...)` Entrypoint
79
73
 
80
- Use `@action(...)` on a script class to register how it should appear in Daz Studio.
74
+ Use `action(...)` at module scope to define how a runnable `.dsa.ts` file should appear in Daz Studio and what it should execute.
81
75
 
82
76
  ```typescript
83
- @action({
77
+ action({
84
78
  text: 'Hello World',
85
79
  menuPath: '#{defaultMenuPath}/Examples',
86
80
  shortcut: 'CTRL+SHIFT+H',
87
81
  toolbar: 'MyToolbar',
88
82
  group: 'Examples',
89
83
  description: 'Runs the Hello World script',
90
- })
91
- class HelloWorldScript extends BaseScript {
92
- protected run(): void {
84
+ }, () => {
85
+ info('Hello World!');
86
+ });
87
+ ```
88
+
89
+ `action(...)` also accepts a reusable class with a `run()` method:
90
+
91
+ ```typescript
92
+ class HelloWorldScript {
93
+ run(): void {
93
94
  info('Hello World!');
94
95
  }
95
96
  }
97
+
98
+ action({ text: 'Hello World' }, HelloWorldScript);
96
99
  ```
97
100
 
98
- Common `@action(...)` parameters:
101
+ Common `action(...)` parameters:
99
102
 
100
103
  - `text`: the label shown for the script in Daz Studio.
101
104
  - `menuPath`: the menu path where the script should be added. Set it to `false` to skip adding the script to a menu. If omitted, the default menu from `--menu-path` is used.
@@ -153,14 +156,11 @@ export class MyDialog extends BasicDialog {
153
156
  #### 3. Connect & Use in Your Script
154
157
 
155
158
  ```typescript
156
- import { action } from '@dsf/core/action-decorator';
157
- import { BaseScript } from '@dsf/core/base-script';
159
+ import { action } from '@dsf/core/action';
158
160
  import { getSelectedNode } from '@dsf/helpers/scene-helper';
159
161
  import { MyDialog, MyDialogModel } from './my-dialog';
160
162
 
161
- @action({ text: 'My Dialog Script' })
162
- class MyDialogScript extends BaseScript {
163
- protected run(): void {
163
+ action({ text: 'My Dialog Script' }, () => {
164
164
  const model = new MyDialogModel();
165
165
  const selectedNode = getSelectedNode();
166
166
 
@@ -185,10 +185,7 @@ class MyDialogScript extends BaseScript {
185
185
  } else {
186
186
  console.log('Dialog cancelled');
187
187
  }
188
- }
189
- }
190
-
191
- new MyDialogScript().exec();
188
+ });
192
189
  ```
193
190
 
194
191
  ### Core Concepts
@@ -325,15 +322,15 @@ This project uses **semantic-release** for automatic versioning and npm publishi
325
322
 
326
323
  Use conventional commit messages to control version bumping:
327
324
 
328
- - **`fix: description`** → Patch version bump (0.1.150.1.16)
325
+ - **`fix: description`** → Patch version bump (`x.y.z``x.y.(z+1)`)
329
326
  - Bug fixes, patches, or minor improvements
330
327
  - Example: `fix: resolve dialog builder layout issue`
331
328
 
332
- - **`feat: description`** → Minor version bump (0.1.150.2.0)
329
+ - **`feat: description`** → Minor version bump (`x.y.z``x.(y+1).0`)
333
330
  - New features or significant enhancements
334
331
  - Example: `feat: add tree view builder component`
335
332
 
336
- - **`BREAKING CHANGE: description`** → Major version bump (0.1.15 → 1.0.0)
333
+ - **`BREAKING CHANGE: description`** → Major version bump (`x.y.z``(x+1).0.0`)
337
334
  - Add to commit body for breaking changes
338
335
  - Example: `feat: refactor action decorator API\n\nBREAKING CHANGE: action() now requires explicit menu path`
339
336
 
@@ -1,9 +1,9 @@
1
- const tsFileParser = require('ts-file-parser');
1
+ const ts = require('typescript');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const glob = require('glob');
5
5
 
6
- const nameofActionDecorator = 'action';
6
+ const nameofActionFunction = 'action';
7
7
 
8
8
  function getPartialPath(filePath) {
9
9
  const fileInfo = path.parse(filePath);
@@ -35,95 +35,192 @@ uninstall(${data});
35
35
  `;
36
36
  }
37
37
 
38
- function processScript(filePath, container, defaultMenuPath) {
39
- const fileInfo = path.parse(filePath);
40
- const content = fs.readFileSync(filePath, 'utf-8').toString();
41
- const json = tsFileParser.parseStruct(content, {}, filePath);
38
+ function literalValue(node) {
39
+ if (!node) {
40
+ return undefined;
41
+ }
42
42
 
43
- json.classes.forEach((cls) => {
44
- const actionDecorator = cls.decorators.find(
45
- (d) => d.name === nameofActionDecorator
46
- );
43
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
44
+ return node.text;
45
+ }
46
+
47
+ if (node.kind === ts.SyntaxKind.TrueKeyword) {
48
+ return true;
49
+ }
50
+
51
+ if (node.kind === ts.SyntaxKind.FalseKeyword) {
52
+ return false;
53
+ }
54
+
55
+ if (ts.isNumericLiteral(node)) {
56
+ return Number(node.text);
57
+ }
47
58
 
48
- if (!actionDecorator) return;
49
-
50
- const decorator = actionDecorator.arguments[0] ?? {};
51
-
52
- const script = {
53
- name: null,
54
- text: fileInfo.name.replace('.dsa', ''),
55
- filePath:
56
- decorator.bundle === undefined
57
- ? getPartialPath(filePath)
58
- : fileInfo.name.replace('.ts', ''),
59
- menuPath: (() => {
60
- const relativeDir = path.parse(getPartialPath(filePath)).dir;
61
- return relativeDir && relativeDir !== '.'
62
- ? `${defaultMenuPath}${relativeDir}`
63
- : defaultMenuPath.replace(/\/$/, '');
64
- })(),
65
- description: fileInfo.name.replace('.dsa', ''),
66
- group: stringOrDefault(decorator.group, undefined),
67
- shortcut: stringOrDefault(decorator.shortcut),
68
- toolbar: stringOrDefault(decorator.toolbar, undefined),
69
- };
70
-
71
- const icon = filePath.replace('.ts', '.png');
72
- if (fs.existsSync(icon)) {
73
- script.icon =
74
- decorator.bundle === undefined
75
- ? `${getPartialPath(filePath)}.png`
76
- : fileInfo.name.replace('.dsa', '.dsa.png');
59
+ if (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand)) {
60
+ if (node.operator === ts.SyntaxKind.MinusToken) {
61
+ return -Number(node.operand.text);
77
62
  }
78
63
 
79
- if (actionDecorator) {
80
- script.text = stringOrDefault(decorator.text, script.text);
81
- if (typeof decorator.menuPath === 'boolean') {
82
- script.menuPath = decorator.menuPath === true ? script.menuPath : '';
83
- } else {
84
- script.menuPath = stringOrDefault(decorator.menuPath, script.menuPath);
85
- }
86
- script.menuPath = script.menuPath.replace(
87
- '#{defaultMenuPath}',
88
- defaultMenuPath
89
- );
90
- script.shortcut = decorator.shortcut;
64
+ if (node.operator === ts.SyntaxKind.PlusToken) {
65
+ return Number(node.operand.text);
91
66
  }
67
+ }
68
+
69
+ return undefined;
70
+ }
71
+
72
+ function objectLiteralToObject(node) {
73
+ if (!node || !ts.isObjectLiteralExpression(node)) {
74
+ return null;
75
+ }
76
+
77
+ const result = {};
92
78
 
93
- console.log(`Adding script: ${script.text}`, script);
94
- container.scripts.push(script);
95
-
96
- // Check if the decorator has a "bundle" property
97
- if (decorator.bundle !== undefined) {
98
- let packageInstallerFilePath = `Install.dsa.ts`;
99
- let packageUninstallerFilePath = `Uninstall.dsa.ts`;
100
-
101
- if (decorator.bundle !== true) {
102
- // If decorator.bundle is a string, use it as the bundle file name
103
- packageInstallerFilePath = `Install ${decorator.bundle}.dsa.ts`;
104
- packageUninstallerFilePath = `Uninstall ${decorator.bundle}.dsa.ts`;
105
- }
106
-
107
- // Generate a separate file with the specified or default name
108
- let bundleScriptContent = generateInstallerTemplate(
109
- JSON.stringify(container.scripts, null, 4)
110
- );
111
- let bundleScriptFilePath = path.join(
112
- path.parse(filePath).dir,
113
- packageInstallerFilePath
114
- );
115
- fs.writeFileSync(bundleScriptFilePath, bundleScriptContent);
116
-
117
- bundleScriptContent = generateUninstallerTemplate(
118
- JSON.stringify(container.scripts, null, 4)
119
- );
120
- bundleScriptFilePath = path.join(
121
- path.parse(filePath).dir,
122
- packageUninstallerFilePath
123
- );
124
- fs.writeFileSync(bundleScriptFilePath, bundleScriptContent);
79
+ node.properties.forEach((property) => {
80
+ if (!ts.isPropertyAssignment(property)) {
81
+ return;
82
+ }
83
+
84
+ const name = ts.isIdentifier(property.name)
85
+ ? property.name.text
86
+ : ts.isStringLiteral(property.name)
87
+ ? property.name.text
88
+ : null;
89
+
90
+ if (!name) {
91
+ return;
92
+ }
93
+
94
+ const value = literalValue(property.initializer);
95
+ if (value !== undefined) {
96
+ result[name] = value;
125
97
  }
126
98
  });
99
+
100
+ return result;
101
+ }
102
+
103
+ function getCallName(expression) {
104
+ if (ts.isIdentifier(expression)) {
105
+ return expression.text;
106
+ }
107
+
108
+ if (ts.isPropertyAccessExpression(expression)) {
109
+ return expression.name.text;
110
+ }
111
+
112
+ return null;
113
+ }
114
+
115
+ function findTopLevelActionCall(content, filePath) {
116
+ const sourceFile = ts.createSourceFile(
117
+ filePath,
118
+ content,
119
+ ts.ScriptTarget.Latest,
120
+ true,
121
+ ts.ScriptKind.TS
122
+ );
123
+
124
+ for (const statement of sourceFile.statements) {
125
+ if (!ts.isExpressionStatement(statement)) {
126
+ continue;
127
+ }
128
+
129
+ const expression = statement.expression;
130
+ if (!ts.isCallExpression(expression)) {
131
+ continue;
132
+ }
133
+
134
+ if (getCallName(expression.expression) !== nameofActionFunction) {
135
+ continue;
136
+ }
137
+
138
+ return expression;
139
+ }
140
+
141
+ return null;
142
+ }
143
+
144
+ function processScript(filePath, container, defaultMenuPath) {
145
+ const fileInfo = path.parse(filePath);
146
+ const content = fs.readFileSync(filePath, 'utf-8').toString();
147
+ const actionCall = findTopLevelActionCall(content, filePath);
148
+
149
+ if (!actionCall) {
150
+ return;
151
+ }
152
+
153
+ const decorator = objectLiteralToObject(actionCall.arguments[0]) ?? {};
154
+
155
+ const script = {
156
+ name: null,
157
+ text: fileInfo.name.replace('.dsa', ''),
158
+ filePath:
159
+ decorator.bundle === undefined
160
+ ? getPartialPath(filePath)
161
+ : fileInfo.name.replace('.ts', ''),
162
+ menuPath: (() => {
163
+ const relativeDir = path.parse(getPartialPath(filePath)).dir;
164
+ return relativeDir && relativeDir !== '.'
165
+ ? `${defaultMenuPath}${relativeDir}`
166
+ : defaultMenuPath.replace(/\/$/, '');
167
+ })(),
168
+ description: fileInfo.name.replace('.dsa', ''),
169
+ group: stringOrDefault(decorator.group, undefined),
170
+ shortcut: stringOrDefault(decorator.shortcut),
171
+ toolbar: stringOrDefault(decorator.toolbar, undefined),
172
+ };
173
+
174
+ const icon = filePath.replace('.ts', '.png');
175
+ if (fs.existsSync(icon)) {
176
+ script.icon =
177
+ decorator.bundle === undefined
178
+ ? `${getPartialPath(filePath)}.png`
179
+ : fileInfo.name.replace('.dsa', '.dsa.png');
180
+ }
181
+
182
+ script.text = stringOrDefault(decorator.text, script.text);
183
+ if (typeof decorator.menuPath === 'boolean') {
184
+ script.menuPath = decorator.menuPath === true ? script.menuPath : '';
185
+ } else {
186
+ script.menuPath = stringOrDefault(decorator.menuPath, script.menuPath);
187
+ }
188
+ script.menuPath = script.menuPath.replace(
189
+ '#{defaultMenuPath}',
190
+ defaultMenuPath
191
+ );
192
+ script.shortcut = decorator.shortcut;
193
+
194
+ console.log(`Adding script: ${script.text}`, script);
195
+ container.scripts.push(script);
196
+
197
+ if (decorator.bundle !== undefined) {
198
+ let packageInstallerFilePath = `Install.dsa.ts`;
199
+ let packageUninstallerFilePath = `Uninstall.dsa.ts`;
200
+
201
+ if (decorator.bundle !== true) {
202
+ packageInstallerFilePath = `Install ${decorator.bundle}.dsa.ts`;
203
+ packageUninstallerFilePath = `Uninstall ${decorator.bundle}.dsa.ts`;
204
+ }
205
+
206
+ let bundleScriptContent = generateInstallerTemplate(
207
+ JSON.stringify(container.scripts, null, 4)
208
+ );
209
+ let bundleScriptFilePath = path.join(
210
+ path.parse(filePath).dir,
211
+ packageInstallerFilePath
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
+ );
222
+ fs.writeFileSync(bundleScriptFilePath, bundleScriptContent);
223
+ }
127
224
  }
128
225
 
129
226
  function processScripts(paths, container, defaultMenuPath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dazscript-framework",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "author": "Freddy Diaz",
5
5
  "license": "MPL-2.0",
6
6
  "description": "",
@@ -1,16 +1 @@
1
- class CustomActionDefinition {
2
- text?: string
3
- description?: string
4
- icon?: string
5
- menuPath?: string | boolean
6
- toolbar?: string
7
- sort?: number
8
- group?: string
9
- shortcut?: string
10
- bundle?: string | boolean
11
- }
12
-
13
- export const action = (action?: CustomActionDefinition) => {
14
- return (target: Function) => {
15
- }
16
- }
1
+ export { ActionDefinition, action } from './action';
@@ -0,0 +1,71 @@
1
+ import { debug, error, raise } from '@dsf/common/log';
2
+
3
+ export class ActionDefinition {
4
+ text?: string
5
+ description?: string
6
+ icon?: string
7
+ menuPath?: string | boolean
8
+ toolbar?: string
9
+ sort?: number
10
+ group?: string
11
+ shortcut?: string
12
+ bundle?: string | boolean
13
+ }
14
+
15
+ type ActionCallback = () => void
16
+ type ActionRunnable = { [key: string]: unknown }
17
+ type ActionConstructor = new (...args: any[]) => any
18
+ type ActionTarget = ActionCallback | ActionRunnable | ActionConstructor
19
+
20
+ const isFunction = (value: unknown): value is Function => typeof value === 'function'
21
+
22
+ const isConstructorTarget = (target: ActionTarget): target is ActionConstructor => {
23
+ if (!isFunction(target)) {
24
+ return false
25
+ }
26
+
27
+ const prototype = (target as ActionConstructor).prototype as ActionRunnable | undefined
28
+ return !!prototype && (typeof prototype.run === 'function' || typeof prototype.exec === 'function')
29
+ }
30
+
31
+ const invokeRunnable = (target: ActionRunnable) => {
32
+ if (typeof target.run === 'function') {
33
+ target.run()
34
+ return
35
+ }
36
+
37
+ if (typeof target.exec === 'function') {
38
+ target.exec()
39
+ return
40
+ }
41
+
42
+ throw new Error('Action target must expose run() or exec().')
43
+ }
44
+
45
+ export function action(definition: ActionDefinition, target: ActionTarget): void {
46
+ const scriptName = definition?.text || 'Unnamed Script'
47
+
48
+ try {
49
+ const runnable = isConstructorTarget(target)
50
+ ? new target()
51
+ : target
52
+
53
+ const announceExecution = !isFunction(runnable) && runnable.anounceExecution === false
54
+ ? false
55
+ : true
56
+
57
+ if (announceExecution) {
58
+ debug(`=== Running script "${scriptName}" ===`)
59
+ }
60
+
61
+ if (isFunction(runnable)) {
62
+ runnable()
63
+ return
64
+ }
65
+
66
+ invokeRunnable(runnable)
67
+ } catch (err) {
68
+ error(`There was an error while running the script "${scriptName}"`)
69
+ raise(err)
70
+ }
71
+ }
@@ -1,13 +1,8 @@
1
1
  import { debug } from '@dsf/common/log';
2
- import { action } from '@dsf/core/action-decorator';
3
- import { BaseScript } from '@dsf/core/base-script';
2
+ import { action } from '@dsf/core/action';
4
3
  import { info } from '@dsf/helpers/message-box-helper';
5
4
 
6
- @action({ text: 'Hello World' })
7
- class HelloWorldScript extends BaseScript {
8
- protected run(): void {
9
- debug('Hello World!');
10
- info('Hello World!');
11
- }
12
- }
13
- new HelloWorldScript().exec();
5
+ action({ text: 'Hello World' }, () => {
6
+ debug('Hello World!');
7
+ info('Hello World!');
8
+ })
@@ -1,52 +1,47 @@
1
1
  import { debug, dump } from '@dsf/common/log';
2
- import { action } from '@dsf/core/action-decorator';
3
- import { BaseScript } from '@dsf/core/base-script';
2
+ import { action } from '@dsf/core/action';
4
3
  import { error } from '@dsf/helpers/message-box-helper';
5
4
  import { getNodes, getSelectedNode } from '@dsf/helpers/scene-helper';
6
5
  import { SampleDialog, SampleDialogModel } from './sample-dialog';
7
6
 
8
- @action({ text: 'Sample Dialog' })
9
- class SampleDialogScript extends BaseScript {
10
- protected run(): void {
11
- let sceneNodes = getNodes()
12
- let selectedNode = getSelectedNode()
13
- if (!selectedNode) {
14
- error('Please select a node.')
15
- return
16
- }
17
-
18
- let model = new SampleDialogModel()
19
- model.nodes$.value = sceneNodes
20
-
21
- model.nodeLabel$.connect((label) => {
22
- if (label === model.selectedNode$.value.getLabel()) return
23
- model.selectedNode$.value.setLabel(label)
24
- model.nodes$.value = getNodes()
25
- })
26
-
27
- let dialog = new SampleDialog(model)
28
-
29
- model.selectedNode$.connect((node) => {
30
- if (!node) return
31
- debug(`Selected Node: ${node.getLabel()}`)
32
- node.select(true)
33
- model.nodeLabel$.value = node.getLabel()
34
- model.showNode$.value = node.isVisible()
35
- model.hideNode$.value = !node.isVisible()
36
- })
37
- dialog.onNodeVisibilityChanged = (node, visible) => {
38
- node.setVisible(visible)
39
- }
40
-
41
- model.selectedNode$.value = selectedNode
42
-
43
- if (!dialog.run()) {
44
- debug(`Dialog Cancelled`)
45
- return
46
- }
47
-
48
- debug(`Dialog Closed`)
49
- dump(model)
7
+ action({ text: 'Sample Dialog' }, () => {
8
+ let sceneNodes = getNodes()
9
+ let selectedNode = getSelectedNode()
10
+ if (!selectedNode) {
11
+ error('Please select a node.')
12
+ return
50
13
  }
51
- }
52
- new SampleDialogScript().exec();
14
+
15
+ let model = new SampleDialogModel()
16
+ model.nodes$.value = sceneNodes
17
+
18
+ model.nodeLabel$.connect((label) => {
19
+ if (label === model.selectedNode$.value.getLabel()) return
20
+ model.selectedNode$.value.setLabel(label)
21
+ model.nodes$.value = getNodes()
22
+ })
23
+
24
+ let dialog = new SampleDialog(model)
25
+
26
+ model.selectedNode$.connect((node) => {
27
+ if (!node) return
28
+ debug(`Selected Node: ${node.getLabel()}`)
29
+ node.select(true)
30
+ model.nodeLabel$.value = node.getLabel()
31
+ model.showNode$.value = node.isVisible()
32
+ model.hideNode$.value = !node.isVisible()
33
+ })
34
+ dialog.onNodeVisibilityChanged = (node, visible) => {
35
+ node.setVisible(visible)
36
+ }
37
+
38
+ model.selectedNode$.value = selectedNode
39
+
40
+ if (!dialog.run()) {
41
+ debug(`Dialog Cancelled`)
42
+ return
43
+ }
44
+
45
+ debug(`Dialog Closed`)
46
+ dump(model)
47
+ })