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