@triflare/fx 0.0.0-stage → 1.0.0
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/LICENSE +201 -0
- package/README.md +73 -2
- package/bin/fx.js +949 -0
- package/package.json +64 -4
- package/product.yml +28 -0
- package/src/brand.js +105 -0
- package/src/build.js +181 -0
- package/src/catalog.js +753 -0
- package/src/compiler.js +1658 -0
- package/src/config-edit.js +338 -0
- package/src/config.js +400 -0
- package/src/decompiler.js +504 -0
- package/src/dev.js +390 -0
- package/src/errors.js +56 -0
- package/src/extensions.js +707 -0
- package/src/index.js +31 -0
- package/src/language.js +12 -0
- package/src/lexer.js +202 -0
- package/src/log.js +70 -0
- package/src/package.js +202 -0
- package/src/packager.js +177 -0
- package/src/parser.js +608 -0
- package/src/product.js +27 -0
- package/src/project.js +156 -0
- package/src/sb3.js +340 -0
- package/src/sprite.js +81 -0
- package/vendor/pm-packager-dist/.fx-build.json +6 -0
- package/vendor/pm-packager-dist/LICENSE +202 -0
- package/vendor/pm-packager-dist/NOTICE +6 -0
- package/vendor/pm-packager-dist/dist/290e09e569a1cab8e61ba93b0d23863f.png +0 -0
- package/vendor/pm-packager-dist/dist/icns.js +1 -0
- package/vendor/pm-packager-dist/dist/packager.js +154 -0
- package/vendor/pm-packager-dist/dist/scaffolding/addons.js +2 -0
- package/vendor/pm-packager-dist/dist/scaffolding/extension worker.js +2 -0
- package/vendor/pm-packager-dist/dist/scaffolding/scaffolding-full.js +93 -0
- package/vendor/pm-packager-dist/dist/scaffolding/scaffolding-min.js +93 -0
- package/vendor/pm-packager-dist/dist/scaffolding/vendors~iframe-extension-worker.js +2 -0
- package/vendor/pm-packager-dist/dist/vendors~sha256.js +3 -0
- package/vendor/pm-packager-dist/package.json +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@triflare/fx",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Code PenguinMod projects with text.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"main": "src/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"fx": "bin/fx.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.js",
|
|
13
|
+
"./catalog": "./src/catalog.js",
|
|
14
|
+
"./language": "./src/language.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin",
|
|
18
|
+
"src",
|
|
19
|
+
"vendor/pm-packager-dist",
|
|
20
|
+
"product.yml",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"check": "npm run test; npm run lint; npm run format:check",
|
|
28
|
+
"test": "node --test test/*.test.js",
|
|
29
|
+
"build:example": "node bin/fx.js build examples/player -o build/player.pmp",
|
|
30
|
+
"lint": "eslint .",
|
|
31
|
+
"lint:fix": "eslint . --fix",
|
|
32
|
+
"format": "prettier --write .",
|
|
33
|
+
"format:check": "prettier --check .",
|
|
34
|
+
"build": "npm run build:packager",
|
|
35
|
+
"build:packager": "node scripts/build-packager.js",
|
|
36
|
+
"prepack": "node scripts/build-packager.js --if-missing"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"js-yaml": "^5.4.3",
|
|
40
|
+
"quickjs-emscripten": "^0.32.0",
|
|
41
|
+
"@fiahfy/icns": "0.0.7",
|
|
42
|
+
"@turbowarp/sbdl": "^7.0.0",
|
|
43
|
+
"cross-fetch": "^4.1.0",
|
|
44
|
+
"jszip": "^3.10.1",
|
|
45
|
+
"sha.js": "^2.4.12"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@eslint/js": "^10.0.1",
|
|
49
|
+
"eslint": "^10.14.0",
|
|
50
|
+
"eslint-config-prettier": "^10.1.8",
|
|
51
|
+
"globals": "^17.13.0",
|
|
52
|
+
"prettier": "^3.9.10",
|
|
53
|
+
"scratch-vm": "github:PenguinMod/PenguinMod-Vm#75ef6994e55270b9c2a2f3cb2edf6a5bf31cdf2a"
|
|
54
|
+
},
|
|
55
|
+
"overrides": {
|
|
56
|
+
"minimatch": "^11.0.4",
|
|
57
|
+
"taffydb": "file:vendor/taffydb",
|
|
58
|
+
"dompurify": "^3.4.17",
|
|
59
|
+
"immutable": "^3.8.4",
|
|
60
|
+
"webpack": "^5.111.1",
|
|
61
|
+
"scratch-parser": {
|
|
62
|
+
"ajv": "^6.15.0",
|
|
63
|
+
"jszip": "^3.10.2"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
package/product.yml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Identity
|
|
2
|
+
name: "Triflare Fx"
|
|
3
|
+
command: "fx"
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
tagline: "Code PenguinMod projects with text."
|
|
6
|
+
author: "Triflare"
|
|
7
|
+
homepage: "https://github.com/triflare/fx"
|
|
8
|
+
|
|
9
|
+
# Branding
|
|
10
|
+
branding:
|
|
11
|
+
colors:
|
|
12
|
+
primary: "#8B5CF6"
|
|
13
|
+
secondary: "#5D1CF2"
|
|
14
|
+
tertiary: "#420BC0"
|
|
15
|
+
|
|
16
|
+
# Terminal Glyphs
|
|
17
|
+
symbols:
|
|
18
|
+
success: "✓"
|
|
19
|
+
error: "✗"
|
|
20
|
+
progress: "~"
|
|
21
|
+
bullet: "•"
|
|
22
|
+
|
|
23
|
+
# Defaults
|
|
24
|
+
defaults:
|
|
25
|
+
configFilename: "fx.yml"
|
|
26
|
+
sourceExtension: ".tfx"
|
|
27
|
+
sourceDirectory: "src"
|
|
28
|
+
outputDirectory: "build"
|
package/src/brand.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { styleText } from "node:util";
|
|
2
|
+
import { loadProduct } from "./product.js";
|
|
3
|
+
|
|
4
|
+
// Every user-facing name, command, and filename comes from product.yml. A
|
|
5
|
+
// rebrand is a one-file change, and a stale literal can't survive a rename.
|
|
6
|
+
export function brand() {
|
|
7
|
+
return loadProduct();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function productName(product = brand()) {
|
|
11
|
+
return product.name;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function commandName(product = brand()) {
|
|
15
|
+
return product.command;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function productVersion(product = brand()) {
|
|
19
|
+
return product.version;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function configFilename(product = brand()) {
|
|
23
|
+
return product.defaults?.configFilename ?? "fx.yml";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function configFilenames(product = brand()) {
|
|
27
|
+
const base = configFilename(product);
|
|
28
|
+
if (base.endsWith(".yml")) return [base, `${base.slice(0, -4)}.yaml`];
|
|
29
|
+
if (base.endsWith(".yaml")) return [base, `${base.slice(0, -5)}.yml`];
|
|
30
|
+
return [base];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function sourceExtension(product = brand()) {
|
|
34
|
+
return product.defaults?.sourceExtension ?? ".tfx";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function sourceDirectory(product = brand()) {
|
|
38
|
+
return product.defaults?.sourceDirectory ?? "src";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function outputDirectory(product = brand()) {
|
|
42
|
+
return product.defaults?.outputDirectory ?? "build";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const FALLBACK_COLORS = {
|
|
46
|
+
primary: "#E8590C",
|
|
47
|
+
secondary: "#BA470A",
|
|
48
|
+
tertiary: "#8B3507",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function brandingColors(product = brand()) {
|
|
52
|
+
const colors = product.branding?.colors ?? {};
|
|
53
|
+
return {
|
|
54
|
+
primary: colors.primary ?? FALLBACK_COLORS.primary,
|
|
55
|
+
secondary: colors.secondary ?? FALLBACK_COLORS.secondary,
|
|
56
|
+
tertiary: colors.tertiary ?? FALLBACK_COLORS.tertiary,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const HEX_COLOR = /^#(?:[\da-f]{3}|[\da-f]{6})$/i;
|
|
61
|
+
|
|
62
|
+
// styleText only learned hex colors in Node 24.16, and engines allow any 22, so
|
|
63
|
+
// the truecolor escape is built here. The gate is styleText's own: ask it to
|
|
64
|
+
// color a marker, and stay plain when it comes back untouched.
|
|
65
|
+
function colorize(text, color, stream) {
|
|
66
|
+
if (!HEX_COLOR.test(color)) return styleText(color, text, { stream });
|
|
67
|
+
if (!styleText("red", "\u0000", { stream }).includes("\u001b")) return text;
|
|
68
|
+
const digits =
|
|
69
|
+
color.length === 4 ? color.slice(1).replace(/./g, "$&$&") : color.slice(1);
|
|
70
|
+
const [red, green, blue] = digits
|
|
71
|
+
.match(/../g)
|
|
72
|
+
.map((part) => parseInt(part, 16));
|
|
73
|
+
return `\u001b[38;2;${red};${green};${blue}m${text}\u001b[39m`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Paints a product name or command in three equal bands taken straight from the
|
|
77
|
+
// logo palette. The colors disappear when the stream can't render them, so
|
|
78
|
+
// piped output and CI logs stay plain text.
|
|
79
|
+
export function brandText(text, stream = process.stdout) {
|
|
80
|
+
const { primary, secondary, tertiary } = brandingColors();
|
|
81
|
+
const palette = [primary, secondary, tertiary];
|
|
82
|
+
const chars = Array.from(text);
|
|
83
|
+
if (chars.length === 0) return text;
|
|
84
|
+
|
|
85
|
+
const bandFor = (index) =>
|
|
86
|
+
Math.min(
|
|
87
|
+
palette.length - 1,
|
|
88
|
+
Math.floor((index * palette.length) / chars.length),
|
|
89
|
+
);
|
|
90
|
+
const runs = [];
|
|
91
|
+
let start = 0;
|
|
92
|
+
for (let index = 1; index <= chars.length; index += 1) {
|
|
93
|
+
if (index === chars.length || bandFor(index) !== bandFor(start)) {
|
|
94
|
+
runs.push(
|
|
95
|
+
colorize(
|
|
96
|
+
chars.slice(start, index).join(""),
|
|
97
|
+
palette[bandFor(start)],
|
|
98
|
+
stream,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
start = index;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return runs.join("");
|
|
105
|
+
}
|
package/src/build.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High level build pipeline: `.tfx` source(s) + `fx.yml` -> `.pmp` bytes.
|
|
3
|
+
*/
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { parse } from "./parser.js";
|
|
7
|
+
import { mergePrograms, discoverSources } from "./project.js";
|
|
8
|
+
import { Compiler } from "./compiler.js";
|
|
9
|
+
import { zipSync } from "./sb3.js";
|
|
10
|
+
import { loadConfigFrom, DEFAULT_PROJECT, DEFAULT_FX } from "./config.js";
|
|
11
|
+
import { sourceExtension } from "./brand.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} BuildOptions
|
|
15
|
+
* @property {string} [source] Triflare Fx source text.
|
|
16
|
+
* @property {string} [file] Path of a single source file.
|
|
17
|
+
* @property {string[]} [files] Paths of several source files.
|
|
18
|
+
* @property {Array<{file: string, source: string}>} [sources] Pre-read sources.
|
|
19
|
+
* @property {string} [dir] Directory to scan for `.tfx` files when no input is given.
|
|
20
|
+
* @property {object} [projectConfig] Explicit project config (overrides fx.yml).
|
|
21
|
+
* @property {object} [fxConfig] Explicit Fx-only config (overrides fx.yml).
|
|
22
|
+
* @property {string} [configDir] Directory to start the fx.yml search from.
|
|
23
|
+
* @property {Record<string, object>} [catalog] Extra catalog entries.
|
|
24
|
+
* @property {Record<string, string>} [extensionUrls] Extension id -> URL map.
|
|
25
|
+
* @property {boolean} [useConfigFile] Whether to auto-load fx.yml (default true).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the list of source files (and their text) to compile.
|
|
30
|
+
* @param {BuildOptions} options
|
|
31
|
+
* @returns {{dir: string, entries: Array<{file: string, source: string}>}}
|
|
32
|
+
*/
|
|
33
|
+
function resolveSources(options) {
|
|
34
|
+
const cwd = process.cwd();
|
|
35
|
+
|
|
36
|
+
if (Array.isArray(options.sources)) {
|
|
37
|
+
const dir = path.resolve(options.dir || cwd);
|
|
38
|
+
const entries = options.sources.map((entry) => {
|
|
39
|
+
if (typeof entry === "string") {
|
|
40
|
+
const file = path.resolve(entry);
|
|
41
|
+
return { file, source: fs.readFileSync(file, "utf8") };
|
|
42
|
+
}
|
|
43
|
+
const file = path.resolve(
|
|
44
|
+
entry.file || path.join(dir, `input${sourceExtension()}`),
|
|
45
|
+
);
|
|
46
|
+
return { file, source: entry.source };
|
|
47
|
+
});
|
|
48
|
+
return { dir, entries };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (options.source != null) {
|
|
52
|
+
const file = options.file
|
|
53
|
+
? path.resolve(options.file)
|
|
54
|
+
: path.join(
|
|
55
|
+
path.resolve(options.dir || cwd),
|
|
56
|
+
`input${sourceExtension()}`,
|
|
57
|
+
);
|
|
58
|
+
return {
|
|
59
|
+
dir: options.dir ? path.resolve(options.dir) : path.dirname(file),
|
|
60
|
+
entries: [{ file, source: options.source }],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(options.files) && options.files.length > 0) {
|
|
65
|
+
const files = options.files.map((f) => path.resolve(f));
|
|
66
|
+
const dir = options.dir
|
|
67
|
+
? path.resolve(options.dir)
|
|
68
|
+
: path.dirname(files[0]);
|
|
69
|
+
return {
|
|
70
|
+
dir,
|
|
71
|
+
entries: files.map((file) => ({
|
|
72
|
+
file,
|
|
73
|
+
source: fs.readFileSync(file, "utf8"),
|
|
74
|
+
})),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (options.file) {
|
|
79
|
+
const file = path.resolve(options.file);
|
|
80
|
+
return {
|
|
81
|
+
dir: path.dirname(file),
|
|
82
|
+
entries: [{ file, source: fs.readFileSync(file, "utf8") }],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const dir = path.resolve(options.dir || cwd);
|
|
87
|
+
const files = discoverSources(dir);
|
|
88
|
+
if (files.length === 0)
|
|
89
|
+
throw new Error(`No ${sourceExtension()} files found under ${dir}`);
|
|
90
|
+
return {
|
|
91
|
+
dir,
|
|
92
|
+
entries: files.map((file) => ({
|
|
93
|
+
file,
|
|
94
|
+
source: fs.readFileSync(file, "utf8"),
|
|
95
|
+
})),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {BuildOptions} [options]
|
|
101
|
+
* @returns {{project: object, assets: Map<string, Buffer>, sb3: Buffer, config: object, fx: object, ast: object, files: string[]}}
|
|
102
|
+
*/
|
|
103
|
+
export function build(options = {}) {
|
|
104
|
+
const { dir, entries } = resolveSources(options);
|
|
105
|
+
if (entries.length === 0)
|
|
106
|
+
throw new Error("No Triflare Fx sources to compile");
|
|
107
|
+
|
|
108
|
+
let projectConfig = options.projectConfig || null;
|
|
109
|
+
let fxConfig = options.fxConfig || null;
|
|
110
|
+
let configPath = null;
|
|
111
|
+
if (!projectConfig && options.useConfigFile !== false) {
|
|
112
|
+
const loaded = loadConfigFrom(options.configDir || dir);
|
|
113
|
+
projectConfig = loaded.project;
|
|
114
|
+
fxConfig = loaded.fx;
|
|
115
|
+
configPath = loaded.path;
|
|
116
|
+
}
|
|
117
|
+
if (!projectConfig) projectConfig = { ...DEFAULT_PROJECT };
|
|
118
|
+
if (!fxConfig) fxConfig = { ...DEFAULT_FX };
|
|
119
|
+
|
|
120
|
+
const asts = entries.map(({ file, source }) => ({
|
|
121
|
+
ast: parse(source, file),
|
|
122
|
+
file,
|
|
123
|
+
}));
|
|
124
|
+
const ast = mergePrograms(asts, {
|
|
125
|
+
partialSprites: fxConfig.partialSprites,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Assets are resolved relative to the file that references them, falling
|
|
129
|
+
// back to the config directory (or the scanned directory) for inline
|
|
130
|
+
// sources that have no path of their own.
|
|
131
|
+
const baseDir = configPath ? path.dirname(configPath) : dir;
|
|
132
|
+
const loadAsset = (assetPath, loc) => {
|
|
133
|
+
const from = loc && loc.file ? path.dirname(loc.file) : baseDir;
|
|
134
|
+
const resolved = path.isAbsolute(assetPath)
|
|
135
|
+
? assetPath
|
|
136
|
+
: path.resolve(from, assetPath);
|
|
137
|
+
return fs.readFileSync(resolved);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const compiler = new Compiler({
|
|
141
|
+
project: projectConfig,
|
|
142
|
+
loadAsset,
|
|
143
|
+
catalog: options.catalog,
|
|
144
|
+
extensionUrls: options.extensionUrls,
|
|
145
|
+
file: entries[0].file,
|
|
146
|
+
});
|
|
147
|
+
const { project, assets } = compiler.compile(ast);
|
|
148
|
+
|
|
149
|
+
const sb3 = writeSb3(project, assets);
|
|
150
|
+
return {
|
|
151
|
+
project,
|
|
152
|
+
assets,
|
|
153
|
+
sb3,
|
|
154
|
+
config: projectConfig,
|
|
155
|
+
fx: fxConfig,
|
|
156
|
+
ast,
|
|
157
|
+
files: entries.map((e) => e.file),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Serialize project JSON + assets into a `.pmp` ZIP buffer.
|
|
163
|
+
*
|
|
164
|
+
* PenguinMod's `.pmp` files are byte-for-byte the same Scratch 3 archive as
|
|
165
|
+
* `.sb3`, so this is also what {@link writeSb3} produces.
|
|
166
|
+
* @param {object} project
|
|
167
|
+
* @param {Map<string, Buffer>} assets
|
|
168
|
+
* @returns {Buffer}
|
|
169
|
+
*/
|
|
170
|
+
export function writeSb3(project, assets) {
|
|
171
|
+
const files = [
|
|
172
|
+
{
|
|
173
|
+
name: "project.json",
|
|
174
|
+
data: Buffer.from(JSON.stringify(project)),
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
for (const [md5ext, buffer] of assets) {
|
|
178
|
+
files.push({ name: md5ext, data: buffer });
|
|
179
|
+
}
|
|
180
|
+
return zipSync(files);
|
|
181
|
+
}
|