initial commit
This commit is contained in:
commit
5e6b69af34
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
73
README.md
Normal file
73
README.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# tb-harmony-modern-ts
|
||||||
|
|
||||||
|
This repository is a project template that allows you to code in modern TypeScript for ToonBoom Harmony.
|
||||||
|
|
||||||
|
## Example script
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function* recursiveIterNodes(n: string): Generator<string> {
|
||||||
|
yield n;
|
||||||
|
if (node.isGroup(n)) {
|
||||||
|
for (let i = 0; i < node.numberOfSubNodes(n); i++) {
|
||||||
|
yield* recursiveIterNodes(node.subNode(n, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const nodes = Array.from(recursiveIterNodes(node.root()));
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
console.log(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
You can script in two ways for ToonBoom Harmony: either with [Python](https://docs.toonboom.com/help/harmony-24/scripting/pythonmodule/index.html) or [QtScript](https://docs.toonboom.com/help/harmony-24/scripting/script/index.html) (JavaScript).
|
||||||
|
|
||||||
|
[QtScript](https://doc.qt.io/archives/qt-5.15/qtscript-index.html) (scripting engine part of the Qt framework) is an old JavaScript dialect that is based on the [ECMAScript 3.0 standard](https://www-archive.mozilla.org/js/language/e262-3.pdf), therefore it lacks features like modules, modern syntax, arrow functions, etc...
|
||||||
|
|
||||||
|
Today we have [TypeScript](https://www.typescriptlang.org/) which is a superset of JavaScript with syntax for types, ES modules, async/await and more but how can we use those?
|
||||||
|
|
||||||
|
## How
|
||||||
|
|
||||||
|
### TypeScript and type definitions
|
||||||
|
|
||||||
|
We can code in TypeScript but in order to use Harmony's QtScript API, we need type definitions (`.d.ts` files describing the types).
|
||||||
|
|
||||||
|
Luckily for us, Bryan Fordney made scripts to generate them: [tba-types](https://github.com/bryab/tba-types) (Typescript definitions for Toon Boom Harmony and Storyboard Pro)
|
||||||
|
|
||||||
|
### Modules and imports
|
||||||
|
|
||||||
|
In Harmony's QtScript you can import code from another file in two ways:
|
||||||
|
|
||||||
|
#### `include`
|
||||||
|
|
||||||
|
```js
|
||||||
|
include("module.js");
|
||||||
|
```
|
||||||
|
|
||||||
|
Which include the whole script into the current one, therefore polluting the current namespace.
|
||||||
|
|
||||||
|
#### `exports`
|
||||||
|
|
||||||
|
```js
|
||||||
|
// function.js
|
||||||
|
exports.test = function test() {
|
||||||
|
MessageLog.trace("test");
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// main.js
|
||||||
|
var module = require("./function.js");
|
||||||
|
module.test();
|
||||||
|
```
|
||||||
|
|
||||||
|
Which is a [CommonJS](https://nodejs.org/api/modules.html#modules-commonjs-modules) "like" custom implementation from ToonBoom without the same semantics. It's also used to define [ToonBoom packages](https://docs.toonboom.com/help/harmony-24/scripting/extended/index.html#create_package) by exporting a `configure` function from a `configure.js` file.
|
||||||
|
|
||||||
|
-> In today's modern JavaScript, the official standard package format is [ECMAScript modules](https://nodejs.org/api/esm.html#modules-ecmascript-modules) (or ESM/ES6 modules) with `import`/`export`.
|
||||||
42
bblloader.ts
Normal file
42
bblloader.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import babelLoader from "babel-loader";
|
||||||
|
import { ConfigItem } from "@babel/core";
|
||||||
|
|
||||||
|
export default babelLoader.custom(() => {
|
||||||
|
// Extract the custom options in the custom plugin
|
||||||
|
function myPlugin() {
|
||||||
|
return {
|
||||||
|
visitor: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Passed the loader options.
|
||||||
|
// customOptions({ opt1, opt2, ...loader }) {
|
||||||
|
// return {
|
||||||
|
// // Pull out any custom options that the loader might have.
|
||||||
|
// custom: { opt1, opt2 },
|
||||||
|
//
|
||||||
|
// // Pass the options back with the two custom options removed.
|
||||||
|
// loader,
|
||||||
|
// };
|
||||||
|
// },
|
||||||
|
|
||||||
|
// Passed Babel's 'PartialConfig' object.
|
||||||
|
config(cfg: ConfigItem) {
|
||||||
|
return {
|
||||||
|
...cfg.options,
|
||||||
|
plugins: [
|
||||||
|
...((cfg.options && cfg.options["plugins"]) || []),
|
||||||
|
[myPlugin],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// result(result) {
|
||||||
|
// return {
|
||||||
|
// ...result,
|
||||||
|
// code: result.code + "\n// Generated by some custom loader",
|
||||||
|
// };
|
||||||
|
// },
|
||||||
|
};
|
||||||
|
});
|
||||||
24
bblplugin.ts
Normal file
24
bblplugin.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// Example from: https://github.com/babel/babel/blob/main/packages/babel-plugin-transform-reserved-words/src/index.ts
|
||||||
|
import { declare } from "@babel/helper-plugin-utils";
|
||||||
|
import { types as t, type NodePath } from "@babel/core";
|
||||||
|
import { isIdentifierName } from "@babel/helper-validator-identifier";
|
||||||
|
|
||||||
|
const QT_SCRIPT_RESERVED_WORDS = new Set(["for"]);
|
||||||
|
|
||||||
|
function isQtScriptReservedWord(name: string): boolean {
|
||||||
|
return isIdentifierName(name) && QT_SCRIPT_RESERVED_WORDS.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default declare((_api) => {
|
||||||
|
return {
|
||||||
|
name: "test",
|
||||||
|
visitor: {
|
||||||
|
Identifier(path: NodePath<t.Identifier>) {
|
||||||
|
if (isQtScriptReservedWord(path.node.name)) {
|
||||||
|
console.log(path.node.name);
|
||||||
|
path.scope.rename(path.node.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
4068
package-lock.json
generated
Normal file
4068
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
package.json
Normal file
37
package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "tb-harmony-modern-ts",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Template for coding in modern TypeScript for ToonBoom Harmony",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "cross-env TS_NODE_PROJECT=\"tsconfig.webpack.json\" webpack",
|
||||||
|
"type-check": "tsc",
|
||||||
|
"dev": "npm run build -- --watch"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Joseph HENRY <joseph@autourdeminuit.com> (Autour du Volcan)",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.28.5",
|
||||||
|
"@babel/helper-validator-identifier": "^7.28.5",
|
||||||
|
"@babel/preset-env": "^7.28.5",
|
||||||
|
"@babel/preset-typescript": "^7.28.5",
|
||||||
|
"@types/babel__helper-plugin-utils": "^7.10.3",
|
||||||
|
"@types/babel__helper-validator-identifier": "^7.15.2",
|
||||||
|
"@types/node": "^24.10.0",
|
||||||
|
"@types/webpack": "^5.28.5",
|
||||||
|
"babel-loader": "^10.0.0",
|
||||||
|
"core-js": "^3.46.0",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
|
"replace-text-in-bundle-plugin": "^1.0.3",
|
||||||
|
"tba-types": "github:bryab/tba-types",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"webpack": "^5.102.1",
|
||||||
|
"webpack-cli": "^6.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/scripts/generator.ts
Normal file
20
src/scripts/generator.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const console = { log: MessageLog.trace };
|
||||||
|
|
||||||
|
function* recursiveIterNodes(n: string): Generator<string> {
|
||||||
|
yield n;
|
||||||
|
if (node.isGroup(n)) {
|
||||||
|
for (let i = 0; i < node.numberOfSubNodes(n); i++) {
|
||||||
|
yield* recursiveIterNodes(node.subNode(n, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const nodes = Array.from(recursiveIterNodes(node.root()));
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
console.log(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
33
src/scripts/uniq.ts
Normal file
33
src/scripts/uniq.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
class Node {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
constructor(name: string) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
static root() {
|
||||||
|
return new Node(node.root());
|
||||||
|
}
|
||||||
|
|
||||||
|
get isGroup() {
|
||||||
|
return node.isGroup(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
*subNodesRecursive(): Generator<Node> {
|
||||||
|
for (let i = 0; i < node.numberOfSubNodes(this.name); i++) {
|
||||||
|
const subNode = new Node(node.subNode(this.name, i));
|
||||||
|
yield subNode;
|
||||||
|
yield* subNode.subNodesRecursive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const root = Node.root();
|
||||||
|
|
||||||
|
for (const node of root.subNodesRecursive()) {
|
||||||
|
MessageLog.trace(node.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"module": "nodenext",
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"target": "esnext",
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"types": ["tba-types/Harmony/24"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
12
tsconfig.webpack.json
Normal file
12
tsconfig.webpack.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// TypeScript config for Webpack configuration (webpack.config.ts)
|
||||||
|
// See: https://webpack.js.org/configuration/configuration-languages/#typescript
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"lib": ["esnext"],
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["./webpack.config.ts"]
|
||||||
|
}
|
||||||
145
webpack.config.ts
Normal file
145
webpack.config.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import path from "path";
|
||||||
|
import { readdirSync } from "fs";
|
||||||
|
import { Compilation, sources } from "webpack";
|
||||||
|
import type {
|
||||||
|
Configuration,
|
||||||
|
RuleSetRule,
|
||||||
|
Compiler,
|
||||||
|
EntryObject,
|
||||||
|
} from "webpack";
|
||||||
|
|
||||||
|
interface FindReplacePluginOptions {
|
||||||
|
replace: { from: RegExp; to: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class FindReplacePlugin {
|
||||||
|
options: FindReplacePluginOptions;
|
||||||
|
|
||||||
|
constructor(options: FindReplacePluginOptions) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(compiler: Compiler) {
|
||||||
|
const pluginName = FindReplacePlugin.name;
|
||||||
|
const logger = compiler.getInfrastructureLogger(pluginName);
|
||||||
|
|
||||||
|
compiler.hooks.compilation.tap({ name: pluginName }, (compilation) => {
|
||||||
|
compilation.hooks.processAssets.tap(
|
||||||
|
{
|
||||||
|
name: pluginName,
|
||||||
|
stage: Compilation.PROCESS_ASSETS_STAGE_ANALYSE,
|
||||||
|
},
|
||||||
|
(assets) => {
|
||||||
|
Object.entries(assets).forEach(([pathname, source]) => {
|
||||||
|
if (path.extname(pathname) !== ".js") return;
|
||||||
|
|
||||||
|
let replaced = source.source().toString();
|
||||||
|
|
||||||
|
// For every regex pattern, replace it in the source content
|
||||||
|
for (const { from, to } of this.options.replace) {
|
||||||
|
const matches = replaced.match(from);
|
||||||
|
logger.info(
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
`Found ${matches ? matches.length : 0} matches`,
|
||||||
|
matches,
|
||||||
|
);
|
||||||
|
|
||||||
|
replaced = replaced.replace(from, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSource = new sources.RawSource(replaced);
|
||||||
|
|
||||||
|
const previousSize = source.size();
|
||||||
|
compilation.updateAsset(pathname, newSource);
|
||||||
|
const newSize = newSource.size();
|
||||||
|
|
||||||
|
if (newSize !== previousSize) {
|
||||||
|
logger.info(
|
||||||
|
`Found and replaced in ${pathname}: ${previousSize} -> ${newSize}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const babelRule: RuleSetRule = {
|
||||||
|
test: /\.(ts|js)$/,
|
||||||
|
use: {
|
||||||
|
loader: "babel-loader",
|
||||||
|
options: {
|
||||||
|
targets: "defaults",
|
||||||
|
sourceType: "unambiguous",
|
||||||
|
presets: [
|
||||||
|
["@babel/preset-typescript"],
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
// Use this to cover the lowest possible syntax requirement for QtScript
|
||||||
|
// See: https://browsersl.ist/
|
||||||
|
targets: "cover 100%",
|
||||||
|
useBuiltIns: "usage",
|
||||||
|
corejs: "3.46.0",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamically returns an object of script entries from src/scripts/*.ts files
|
||||||
|
*/
|
||||||
|
function getScriptsEntryObject(): EntryObject {
|
||||||
|
const entries: EntryObject = {};
|
||||||
|
const scriptsPath = path.resolve(__dirname, "src", "scripts");
|
||||||
|
const scripts = readdirSync(scriptsPath).filter((file) =>
|
||||||
|
file.endsWith(".ts"),
|
||||||
|
);
|
||||||
|
|
||||||
|
scripts.forEach((script) => {
|
||||||
|
entries[path.parse(script).name] = path.resolve(scriptsPath, script);
|
||||||
|
});
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: Configuration = {
|
||||||
|
mode: "production",
|
||||||
|
module: {
|
||||||
|
rules: [babelRule],
|
||||||
|
},
|
||||||
|
entry: getScriptsEntryObject(),
|
||||||
|
resolve: { extensions: [".ts", ".js"] },
|
||||||
|
output: {
|
||||||
|
filename: "[name].js",
|
||||||
|
path: path.resolve(__dirname, "dist"),
|
||||||
|
environment: { arrowFunction: false },
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new FindReplacePlugin({
|
||||||
|
replace: [
|
||||||
|
/**
|
||||||
|
* Replaces reserved words as property names
|
||||||
|
* For example obj.for = 0 -> obj["for"] = 0
|
||||||
|
*/
|
||||||
|
{ from: /\.(for|return)([^\w])/g, to: '["$1"]$2' },
|
||||||
|
/**
|
||||||
|
* Replaces reserved words as object literal property names
|
||||||
|
* See: https://eslint.style/rules/quote-props#quote-props
|
||||||
|
* For example { for: 0 } -> { "for": 0 }
|
||||||
|
*/
|
||||||
|
{ from: /(for|return):/g, to: '"$1":' },
|
||||||
|
{ from: /\/(.*)\[([^\]]*)\/([^\]]*)\](.*)\//g, to: "/$1[$2\/$3]$4/" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
optimization: {
|
||||||
|
minimize: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
Loading…
x
Reference in New Issue
Block a user