@steambrew/ttc 3.0.1 → 3.1.2
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/.claude/settings.local.json +2 -1
- package/dist/index.js +125 -14
- package/package.json +2 -1
- package/src/check-health.ts +3 -2
- package/src/index.ts +6 -3
- package/src/logger.ts +5 -0
- package/src/plugin-json.d.ts +16 -0
- package/src/query-parser.ts +10 -2
- package/src/static-embed.ts +55 -9
- package/src/transpiler.ts +73 -4
package/dist/index.js
CHANGED
|
@@ -13,18 +13,18 @@ import terser from '@rollup/plugin-terser';
|
|
|
13
13
|
import typescript from '@rollup/plugin-typescript';
|
|
14
14
|
import url from '@rollup/plugin-url';
|
|
15
15
|
import nodePolyfills from 'rollup-plugin-polyfill-node';
|
|
16
|
-
import { rollup } from 'rollup';
|
|
16
|
+
import { watch, rollup } from 'rollup';
|
|
17
17
|
import { minify_sync } from 'terser';
|
|
18
18
|
import scss from 'rollup-plugin-scss';
|
|
19
19
|
import * as sass from 'sass';
|
|
20
20
|
import dotenv from 'dotenv';
|
|
21
21
|
import injectProcessEnv from 'rollup-plugin-inject-process-env';
|
|
22
|
+
import { performance } from 'perf_hooks';
|
|
22
23
|
import * as parser from '@babel/parser';
|
|
23
24
|
import { createFilter } from '@rollup/pluginutils';
|
|
24
25
|
import * as glob from 'glob';
|
|
25
26
|
import MagicString from 'magic-string';
|
|
26
27
|
import _traverse from '@babel/traverse';
|
|
27
|
-
import { performance as performance$1 } from 'perf_hooks';
|
|
28
28
|
|
|
29
29
|
const version = JSON.parse(readFileSync(path.resolve(dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf8')).version;
|
|
30
30
|
const Logger = {
|
|
@@ -59,6 +59,10 @@ const Logger = {
|
|
|
59
59
|
meta.push(`${envCount} env var${envCount > 1 ? 's' : ''}`);
|
|
60
60
|
console.log(`${chalk.green('Finished')} ${buildType} in ${elapsed} ` + chalk.dim('(' + meta.join(', ') + ')'));
|
|
61
61
|
},
|
|
62
|
+
failed({ elapsedMs, buildType }) {
|
|
63
|
+
const elapsed = `${(elapsedMs / 1000).toFixed(2)}s`;
|
|
64
|
+
console.error(`${chalk.red('Failed')} ${buildType} in ${elapsed} ` + chalk.dim(`(ttc v${version})`));
|
|
65
|
+
},
|
|
62
66
|
};
|
|
63
67
|
|
|
64
68
|
const PrintParamHelp = () => {
|
|
@@ -69,6 +73,7 @@ const PrintParamHelp = () => {
|
|
|
69
73
|
'options:',
|
|
70
74
|
' --build <dev|prod> build type (prod enables minification)',
|
|
71
75
|
' --target <path> plugin directory (default: current directory)',
|
|
76
|
+
' --watch enable watch mode for continuous rebuilding',
|
|
72
77
|
' --no-update skip update check',
|
|
73
78
|
' --help show this message',
|
|
74
79
|
'',
|
|
@@ -80,7 +85,7 @@ var BuildType;
|
|
|
80
85
|
BuildType[BuildType["ProdBuild"] = 1] = "ProdBuild";
|
|
81
86
|
})(BuildType || (BuildType = {}));
|
|
82
87
|
const ValidateParameters = (args) => {
|
|
83
|
-
let typeProp = BuildType.DevBuild, targetProp = process.cwd(), isMillennium = false;
|
|
88
|
+
let typeProp = BuildType.DevBuild, targetProp = process.cwd(), isMillennium = false, watch = false;
|
|
84
89
|
if (args.includes('--help')) {
|
|
85
90
|
PrintParamHelp();
|
|
86
91
|
process.exit();
|
|
@@ -116,11 +121,15 @@ const ValidateParameters = (args) => {
|
|
|
116
121
|
if (args[i] == '--millennium-internal') {
|
|
117
122
|
isMillennium = true;
|
|
118
123
|
}
|
|
124
|
+
if (args[i] === '--watch') {
|
|
125
|
+
watch = true;
|
|
126
|
+
}
|
|
119
127
|
}
|
|
120
128
|
return {
|
|
121
129
|
type: typeProp,
|
|
122
130
|
targetPlugin: targetProp,
|
|
123
|
-
isMillennium
|
|
131
|
+
isMillennium,
|
|
132
|
+
watch,
|
|
124
133
|
};
|
|
125
134
|
};
|
|
126
135
|
|
|
@@ -266,6 +275,7 @@ function constSysfsExpr(options = {}) {
|
|
|
266
275
|
return null;
|
|
267
276
|
const magicString = new MagicString(code);
|
|
268
277
|
let hasReplaced = false;
|
|
278
|
+
let constSysfsImport = null;
|
|
269
279
|
try {
|
|
270
280
|
const stringVariables = new Map();
|
|
271
281
|
const ast = parser.parse(code, {
|
|
@@ -280,6 +290,25 @@ function constSysfsExpr(options = {}) {
|
|
|
280
290
|
stringVariables.set(id.name, init.value);
|
|
281
291
|
}
|
|
282
292
|
},
|
|
293
|
+
ImportDeclaration(nodePath) {
|
|
294
|
+
const decl = nodePath.node;
|
|
295
|
+
const specifiers = decl.specifiers;
|
|
296
|
+
const idx = specifiers.findIndex((s) => s.type === 'ImportSpecifier' && (s.imported?.name === 'constSysfsExpr' || s.local?.name === 'constSysfsExpr'));
|
|
297
|
+
if (idx !== -1 && typeof decl.start === 'number' && typeof decl.end === 'number') {
|
|
298
|
+
const spec = specifiers[idx];
|
|
299
|
+
if (typeof spec.start === 'number' && typeof spec.end === 'number') {
|
|
300
|
+
constSysfsImport = {
|
|
301
|
+
specifierStart: spec.start,
|
|
302
|
+
specifierEnd: spec.end,
|
|
303
|
+
declStart: decl.start,
|
|
304
|
+
declEnd: decl.end,
|
|
305
|
+
isOnlySpecifier: specifiers.length === 1,
|
|
306
|
+
prevSpecifierEnd: idx > 0 && typeof specifiers[idx - 1].end === 'number' ? specifiers[idx - 1].end : null,
|
|
307
|
+
nextSpecifierStart: idx < specifiers.length - 1 && typeof specifiers[idx + 1].start === 'number' ? specifiers[idx + 1].start : null,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
},
|
|
283
312
|
});
|
|
284
313
|
traverse(ast, {
|
|
285
314
|
CallExpression: (nodePath) => {
|
|
@@ -415,9 +444,10 @@ function constSysfsExpr(options = {}) {
|
|
|
415
444
|
fileName: path.relative(searchBasePath, singleFilePath),
|
|
416
445
|
};
|
|
417
446
|
embeddedContent = JSON.stringify(fileInfo);
|
|
447
|
+
this.addWatchFile(singleFilePath);
|
|
418
448
|
}
|
|
419
449
|
catch (fileError) {
|
|
420
|
-
let message = String(fileError instanceof Error ? fileError.message : fileError ?? 'Unknown file read error');
|
|
450
|
+
let message = String(fileError instanceof Error ? fileError.message : (fileError ?? 'Unknown file read error'));
|
|
421
451
|
this.error(`Error reading file ${singleFilePath}: ${message}`, node.loc?.start.index);
|
|
422
452
|
return;
|
|
423
453
|
}
|
|
@@ -438,9 +468,10 @@ function constSysfsExpr(options = {}) {
|
|
|
438
468
|
filePath: fullPath,
|
|
439
469
|
fileName: path.relative(searchBasePath, fullPath),
|
|
440
470
|
});
|
|
471
|
+
this.addWatchFile(fullPath);
|
|
441
472
|
}
|
|
442
473
|
catch (fileError) {
|
|
443
|
-
let message = String(fileError instanceof Error ? fileError.message : fileError ?? 'Unknown file read error');
|
|
474
|
+
let message = String(fileError instanceof Error ? fileError.message : (fileError ?? 'Unknown file read error'));
|
|
444
475
|
this.warn(`Error reading file ${fullPath}: ${message}`);
|
|
445
476
|
}
|
|
446
477
|
}
|
|
@@ -452,7 +483,7 @@ function constSysfsExpr(options = {}) {
|
|
|
452
483
|
count++;
|
|
453
484
|
}
|
|
454
485
|
catch (error) {
|
|
455
|
-
const message = String(error instanceof Error ? error.message : error ?? 'Unknown error during file processing');
|
|
486
|
+
const message = String(error instanceof Error ? error.message : (error ?? 'Unknown error during file processing'));
|
|
456
487
|
this.error(`Could not process files for constSysfsExpr: ${message}`, node.loc?.start.index);
|
|
457
488
|
return;
|
|
458
489
|
}
|
|
@@ -461,10 +492,27 @@ function constSysfsExpr(options = {}) {
|
|
|
461
492
|
});
|
|
462
493
|
}
|
|
463
494
|
catch (error) {
|
|
464
|
-
const message = String(error instanceof Error ? error.message : error ?? 'Unknown parsing error');
|
|
495
|
+
const message = String(error instanceof Error ? error.message : (error ?? 'Unknown parsing error'));
|
|
465
496
|
this.error(`Failed to parse ${id}: ${message}`);
|
|
466
497
|
return null;
|
|
467
498
|
}
|
|
499
|
+
if (constSysfsImport !== null && hasReplaced) {
|
|
500
|
+
const info = constSysfsImport;
|
|
501
|
+
if (info.isOnlySpecifier) {
|
|
502
|
+
let endPos = info.declEnd;
|
|
503
|
+
if (code[endPos] === '\n')
|
|
504
|
+
endPos++;
|
|
505
|
+
else if (code[endPos] === '\r' && code[endPos + 1] === '\n')
|
|
506
|
+
endPos += 2;
|
|
507
|
+
magicString.remove(info.declStart, endPos);
|
|
508
|
+
}
|
|
509
|
+
else if (info.nextSpecifierStart !== null) {
|
|
510
|
+
magicString.remove(info.specifierStart, info.nextSpecifierStart);
|
|
511
|
+
}
|
|
512
|
+
else if (info.prevSpecifierEnd !== null) {
|
|
513
|
+
magicString.remove(info.prevSpecifierEnd, info.specifierEnd);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
468
516
|
// If no replacements were made, return null
|
|
469
517
|
if (!hasReplaced) {
|
|
470
518
|
return null;
|
|
@@ -554,6 +602,8 @@ function stripPluginPrefix(message) {
|
|
|
554
602
|
message = message.replace(/^@?[\w-]+\/[\w-]+\s+/, '');
|
|
555
603
|
return message;
|
|
556
604
|
}
|
|
605
|
+
class BuildFailedError extends Error {
|
|
606
|
+
}
|
|
557
607
|
class MillenniumBuild {
|
|
558
608
|
isExternal(id) {
|
|
559
609
|
const hint = this.forbidden.get(id);
|
|
@@ -563,6 +613,25 @@ class MillenniumBuild {
|
|
|
563
613
|
}
|
|
564
614
|
return this.externals.has(id);
|
|
565
615
|
}
|
|
616
|
+
async watchConfig(input, sysfsPlugin, isMillennium) {
|
|
617
|
+
return {
|
|
618
|
+
input,
|
|
619
|
+
plugins: await this.plugins(sysfsPlugin),
|
|
620
|
+
onwarn: (warning) => {
|
|
621
|
+
const msg = stripPluginPrefix(warning.message);
|
|
622
|
+
const loc = logLocation(warning);
|
|
623
|
+
if (warning.plugin === 'typescript') {
|
|
624
|
+
Logger.error(msg, loc);
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
Logger.warn(msg, loc);
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
context: 'window',
|
|
631
|
+
external: (id) => this.isExternal(id),
|
|
632
|
+
output: this.output(isMillennium),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
566
635
|
async build(input, sysfsPlugin, isMillennium) {
|
|
567
636
|
let hasErrors = false;
|
|
568
637
|
const config = {
|
|
@@ -585,7 +654,7 @@ class MillenniumBuild {
|
|
|
585
654
|
};
|
|
586
655
|
await (await rollup(config)).write(config.output);
|
|
587
656
|
if (hasErrors)
|
|
588
|
-
|
|
657
|
+
throw new BuildFailedError();
|
|
589
658
|
}
|
|
590
659
|
}
|
|
591
660
|
class FrontendBuild extends MillenniumBuild {
|
|
@@ -679,10 +748,47 @@ class WebkitBuild extends MillenniumBuild {
|
|
|
679
748
|
};
|
|
680
749
|
}
|
|
681
750
|
}
|
|
682
|
-
|
|
751
|
+
function RunWatchMode(frontendConfig, webkitConfig) {
|
|
752
|
+
const configs = webkitConfig ? [frontendConfig, webkitConfig] : [frontendConfig];
|
|
753
|
+
const watcher = watch(configs);
|
|
754
|
+
console.log(chalk.blueBright.bold('watch'), 'watching for file changes...');
|
|
755
|
+
watcher.on('event', async (event) => {
|
|
756
|
+
if (event.code === 'BUNDLE_START') {
|
|
757
|
+
const label = event.output.some((f) => f.includes('index.js')) ? 'frontend' : 'webkit';
|
|
758
|
+
console.log(chalk.yellowBright.bold('watch'), `rebuilding ${label}...`);
|
|
759
|
+
}
|
|
760
|
+
else if (event.code === 'BUNDLE_END') {
|
|
761
|
+
const label = event.output.some((f) => f.includes('index.js')) ? 'frontend' : 'webkit';
|
|
762
|
+
console.log(chalk.greenBright.bold('watch'), `${label} built in ${chalk.green(`${event.duration}ms`)}`);
|
|
763
|
+
await event.result.close();
|
|
764
|
+
}
|
|
765
|
+
else if (event.code === 'ERROR') {
|
|
766
|
+
const err = event.error;
|
|
767
|
+
const msg = stripPluginPrefix(err?.message ?? String(err));
|
|
768
|
+
Logger.error(msg, logLocation(err));
|
|
769
|
+
if (event.result)
|
|
770
|
+
await event.result.close();
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
const shutdown = () => {
|
|
774
|
+
console.log(chalk.yellowBright.bold('watch'), 'stopping...');
|
|
775
|
+
watcher.close();
|
|
776
|
+
process.exit(0);
|
|
777
|
+
};
|
|
778
|
+
process.on('SIGINT', shutdown);
|
|
779
|
+
process.on('SIGUSR2', shutdown);
|
|
780
|
+
}
|
|
781
|
+
const TranspilerPluginComponent = async (pluginJson, props) => {
|
|
683
782
|
const webkitDir = './webkit/index.tsx';
|
|
684
783
|
const frontendDir = getFrontendDir(pluginJson);
|
|
685
784
|
const sysfs = constSysfsExpr();
|
|
785
|
+
const isMillennium = props.isMillennium ?? false;
|
|
786
|
+
if (props.watch) {
|
|
787
|
+
const frontendConfig = await new FrontendBuild(frontendDir, props).watchConfig(resolveEntryFile(frontendDir), sysfs.plugin, isMillennium);
|
|
788
|
+
const webkitConfig = fs.existsSync(webkitDir) ? await new WebkitBuild(props).watchConfig(webkitDir, sysfs.plugin, isMillennium) : null;
|
|
789
|
+
RunWatchMode(frontendConfig, webkitConfig);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
686
792
|
try {
|
|
687
793
|
await new FrontendBuild(frontendDir, props).build(resolveEntryFile(frontendDir), sysfs.plugin, isMillennium);
|
|
688
794
|
if (fs.existsSync(webkitDir)) {
|
|
@@ -696,7 +802,10 @@ const TranspilerPluginComponent = async (isMillennium, pluginJson, props) => {
|
|
|
696
802
|
});
|
|
697
803
|
}
|
|
698
804
|
catch (exception) {
|
|
699
|
-
|
|
805
|
+
if (!(exception instanceof BuildFailedError)) {
|
|
806
|
+
Logger.error(stripPluginPrefix(exception?.message ?? String(exception)), logLocation(exception));
|
|
807
|
+
}
|
|
808
|
+
Logger.failed({ elapsedMs: performance.now() - global.PerfStartTime, buildType: props.minify ? 'prod' : 'dev' });
|
|
700
809
|
process.exit(1);
|
|
701
810
|
}
|
|
702
811
|
};
|
|
@@ -717,16 +826,18 @@ const StartCompilerModule = () => {
|
|
|
717
826
|
.then((json) => {
|
|
718
827
|
const props = {
|
|
719
828
|
minify: bTersePlugin,
|
|
720
|
-
pluginName: json
|
|
829
|
+
pluginName: json.name,
|
|
830
|
+
watch: parameters.watch || false,
|
|
831
|
+
isMillennium: bIsMillennium,
|
|
721
832
|
};
|
|
722
|
-
TranspilerPluginComponent(
|
|
833
|
+
TranspilerPluginComponent(json, props);
|
|
723
834
|
})
|
|
724
835
|
.catch(() => {
|
|
725
836
|
process.exit();
|
|
726
837
|
});
|
|
727
838
|
};
|
|
728
839
|
const Initialize = () => {
|
|
729
|
-
global.PerfStartTime = performance
|
|
840
|
+
global.PerfStartTime = performance.now();
|
|
730
841
|
// Check for --no-update flag
|
|
731
842
|
if (process.argv.includes('--no-update')) {
|
|
732
843
|
StartCompilerModule();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steambrew/ttc",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "rollup -c",
|
|
12
|
+
"dev": "rollup -c -w",
|
|
12
13
|
"prepare": "bun run build"
|
|
13
14
|
},
|
|
14
15
|
"publishConfig": {
|
package/src/check-health.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { existsSync, readFile } from 'fs';
|
|
3
3
|
import { Logger } from './logger';
|
|
4
|
+
import { PluginJson } from './plugin-json';
|
|
4
5
|
|
|
5
|
-
export const ValidatePlugin = (bIsMillennium: boolean, target: string): Promise<
|
|
6
|
-
return new Promise<
|
|
6
|
+
export const ValidatePlugin = (bIsMillennium: boolean, target: string): Promise<PluginJson> => {
|
|
7
|
+
return new Promise<PluginJson>((resolve, reject) => {
|
|
7
8
|
if (!existsSync(target)) {
|
|
8
9
|
Logger.error(`target path does not exist: ${target}`);
|
|
9
10
|
reject();
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { BuildType, ValidateParameters } from './query-parser';
|
|
9
9
|
import { CheckForUpdates } from './version-control';
|
|
10
10
|
import { ValidatePlugin } from './check-health';
|
|
11
|
+
import { PluginJson } from './plugin-json';
|
|
11
12
|
import { TranspilerPluginComponent, TranspilerProps } from './transpiler';
|
|
12
13
|
import { performance } from 'perf_hooks';
|
|
13
14
|
|
|
@@ -25,13 +26,15 @@ const StartCompilerModule = () => {
|
|
|
25
26
|
const bTersePlugin = parameters.type == BuildType.ProdBuild;
|
|
26
27
|
|
|
27
28
|
ValidatePlugin(bIsMillennium, parameters.targetPlugin)
|
|
28
|
-
.then((json:
|
|
29
|
+
.then((json: PluginJson) => {
|
|
29
30
|
const props: TranspilerProps = {
|
|
30
31
|
minify: bTersePlugin,
|
|
31
|
-
pluginName: json
|
|
32
|
+
pluginName: json.name,
|
|
33
|
+
watch: parameters.watch || false,
|
|
34
|
+
isMillennium: bIsMillennium,
|
|
32
35
|
};
|
|
33
36
|
|
|
34
|
-
TranspilerPluginComponent(
|
|
37
|
+
TranspilerPluginComponent(json, props);
|
|
35
38
|
})
|
|
36
39
|
.catch(() => {
|
|
37
40
|
process.exit();
|
package/src/logger.ts
CHANGED
|
@@ -44,6 +44,11 @@ const Logger = {
|
|
|
44
44
|
if (envCount) meta.push(`${envCount} env var${envCount > 1 ? 's' : ''}`);
|
|
45
45
|
console.log(`${chalk.green('Finished')} ${buildType} in ${elapsed} ` + chalk.dim('(' + meta.join(', ') + ')'));
|
|
46
46
|
},
|
|
47
|
+
|
|
48
|
+
failed({ elapsedMs, buildType }: Pick<DoneOptions, 'elapsedMs' | 'buildType'>) {
|
|
49
|
+
const elapsed = `${(elapsedMs / 1000).toFixed(2)}s`;
|
|
50
|
+
console.error(`${chalk.red('Failed')} ${buildType} in ${elapsed} ` + chalk.dim(`(ttc v${version})`));
|
|
51
|
+
},
|
|
47
52
|
};
|
|
48
53
|
|
|
49
54
|
export { Logger };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* generated from https://raw.githubusercontent.com/SteamClientHomebrew/Millennium/main/src/sys/plugin-schema.json
|
|
3
|
+
*/
|
|
4
|
+
export interface PluginJson {
|
|
5
|
+
backend?: string;
|
|
6
|
+
common_name?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
frontend?: string;
|
|
9
|
+
include?: string[];
|
|
10
|
+
name: string;
|
|
11
|
+
splash_image?: string;
|
|
12
|
+
thumbnail?: string;
|
|
13
|
+
useBackend?: boolean;
|
|
14
|
+
venv?: string;
|
|
15
|
+
version?: string;
|
|
16
|
+
}
|
package/src/query-parser.ts
CHANGED
|
@@ -9,6 +9,7 @@ export const PrintParamHelp = () => {
|
|
|
9
9
|
'options:',
|
|
10
10
|
' --build <dev|prod> build type (prod enables minification)',
|
|
11
11
|
' --target <path> plugin directory (default: current directory)',
|
|
12
|
+
' --watch enable watch mode for continuous rebuilding',
|
|
12
13
|
' --no-update skip update check',
|
|
13
14
|
' --help show this message',
|
|
14
15
|
'',
|
|
@@ -25,12 +26,14 @@ export interface ParameterProps {
|
|
|
25
26
|
type: BuildType;
|
|
26
27
|
targetPlugin: string; // path
|
|
27
28
|
isMillennium?: boolean;
|
|
29
|
+
watch?: boolean;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export const ValidateParameters = (args: Array<string>): ParameterProps => {
|
|
31
33
|
let typeProp: BuildType = BuildType.DevBuild,
|
|
32
34
|
targetProp: string = process.cwd(),
|
|
33
|
-
isMillennium: boolean = false
|
|
35
|
+
isMillennium: boolean = false,
|
|
36
|
+
watch: boolean = false;
|
|
34
37
|
|
|
35
38
|
if (args.includes('--help')) {
|
|
36
39
|
PrintParamHelp();
|
|
@@ -73,11 +76,16 @@ export const ValidateParameters = (args: Array<string>): ParameterProps => {
|
|
|
73
76
|
if (args[i] == '--millennium-internal') {
|
|
74
77
|
isMillennium = true;
|
|
75
78
|
}
|
|
79
|
+
|
|
80
|
+
if (args[i] === '--watch') {
|
|
81
|
+
watch = true;
|
|
82
|
+
}
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
return {
|
|
79
86
|
type: typeProp,
|
|
80
87
|
targetPlugin: targetProp,
|
|
81
|
-
isMillennium
|
|
88
|
+
isMillennium,
|
|
89
|
+
watch,
|
|
82
90
|
};
|
|
83
91
|
};
|
package/src/static-embed.ts
CHANGED
|
@@ -33,6 +33,16 @@ interface FileInfo {
|
|
|
33
33
|
fileName: string;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
interface ImportSpecifierInfo {
|
|
37
|
+
specifierStart: number;
|
|
38
|
+
specifierEnd: number;
|
|
39
|
+
declStart: number;
|
|
40
|
+
declEnd: number;
|
|
41
|
+
isOnlySpecifier: boolean;
|
|
42
|
+
prevSpecifierEnd: number | null;
|
|
43
|
+
nextSpecifierStart: number | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
export default function constSysfsExpr(options: EmbedPluginOptions = {}): SysfsPlugin {
|
|
37
47
|
const filter = createFilter(options.include, options.exclude);
|
|
38
48
|
const pluginName = 'millennium-const-sysfs-expr';
|
|
@@ -47,6 +57,7 @@ export default function constSysfsExpr(options: EmbedPluginOptions = {}): SysfsP
|
|
|
47
57
|
|
|
48
58
|
const magicString = new MagicString(code);
|
|
49
59
|
let hasReplaced = false;
|
|
60
|
+
let constSysfsImport: ImportSpecifierInfo | null = null;
|
|
50
61
|
|
|
51
62
|
try {
|
|
52
63
|
const stringVariables = new Map<string, string>();
|
|
@@ -64,6 +75,28 @@ export default function constSysfsExpr(options: EmbedPluginOptions = {}): SysfsP
|
|
|
64
75
|
stringVariables.set(id.name, init.value);
|
|
65
76
|
}
|
|
66
77
|
},
|
|
78
|
+
ImportDeclaration(nodePath) {
|
|
79
|
+
const decl = nodePath.node;
|
|
80
|
+
const specifiers = decl.specifiers;
|
|
81
|
+
const idx = specifiers.findIndex(
|
|
82
|
+
(s) => s.type === 'ImportSpecifier' && ((s as any).imported?.name === 'constSysfsExpr' || (s as any).local?.name === 'constSysfsExpr'),
|
|
83
|
+
);
|
|
84
|
+
if (idx !== -1 && typeof decl.start === 'number' && typeof decl.end === 'number') {
|
|
85
|
+
const spec = specifiers[idx];
|
|
86
|
+
if (typeof spec.start === 'number' && typeof spec.end === 'number') {
|
|
87
|
+
constSysfsImport = {
|
|
88
|
+
specifierStart: spec.start,
|
|
89
|
+
specifierEnd: spec.end,
|
|
90
|
+
declStart: decl.start,
|
|
91
|
+
declEnd: decl.end,
|
|
92
|
+
isOnlySpecifier: specifiers.length === 1,
|
|
93
|
+
prevSpecifierEnd: idx > 0 && typeof specifiers[idx - 1].end === 'number' ? (specifiers[idx - 1].end as number) : null,
|
|
94
|
+
nextSpecifierStart:
|
|
95
|
+
idx < specifiers.length - 1 && typeof specifiers[idx + 1].start === 'number' ? (specifiers[idx + 1].start as number) : null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
67
100
|
});
|
|
68
101
|
|
|
69
102
|
traverse(ast, {
|
|
@@ -177,14 +210,13 @@ export default function constSysfsExpr(options: EmbedPluginOptions = {}): SysfsP
|
|
|
177
210
|
}
|
|
178
211
|
|
|
179
212
|
try {
|
|
180
|
-
|
|
181
213
|
const searchBasePath = callOptions.basePath
|
|
182
214
|
? path.isAbsolute(callOptions.basePath)
|
|
183
215
|
? callOptions.basePath
|
|
184
216
|
: path.resolve(path.dirname(id), callOptions.basePath)
|
|
185
217
|
: path.isAbsolute(pathOrPattern) && !/[?*+!@()[\]{}]/.test(pathOrPattern)
|
|
186
|
-
|
|
187
|
-
|
|
218
|
+
? path.dirname(pathOrPattern)
|
|
219
|
+
: path.resolve(path.dirname(id), path.dirname(pathOrPattern));
|
|
188
220
|
|
|
189
221
|
let embeddedContent: string;
|
|
190
222
|
|
|
@@ -206,14 +238,13 @@ export default function constSysfsExpr(options: EmbedPluginOptions = {}): SysfsP
|
|
|
206
238
|
fileName: path.relative(searchBasePath, singleFilePath),
|
|
207
239
|
};
|
|
208
240
|
embeddedContent = JSON.stringify(fileInfo);
|
|
241
|
+
this.addWatchFile(singleFilePath);
|
|
209
242
|
} catch (fileError: unknown) {
|
|
210
|
-
let message = String(fileError instanceof Error ? fileError.message : fileError ?? 'Unknown file read error');
|
|
243
|
+
let message = String(fileError instanceof Error ? fileError.message : (fileError ?? 'Unknown file read error'));
|
|
211
244
|
this.error(`Error reading file ${singleFilePath}: ${message}`, node.loc?.start.index);
|
|
212
245
|
return;
|
|
213
246
|
}
|
|
214
247
|
} else {
|
|
215
|
-
|
|
216
|
-
|
|
217
248
|
const matchingFiles = glob.sync(pathOrPattern, {
|
|
218
249
|
cwd: searchBasePath,
|
|
219
250
|
nodir: true,
|
|
@@ -230,8 +261,9 @@ export default function constSysfsExpr(options: EmbedPluginOptions = {}): SysfsP
|
|
|
230
261
|
filePath: fullPath,
|
|
231
262
|
fileName: path.relative(searchBasePath, fullPath),
|
|
232
263
|
});
|
|
264
|
+
this.addWatchFile(fullPath);
|
|
233
265
|
} catch (fileError: unknown) {
|
|
234
|
-
let message = String(fileError instanceof Error ? fileError.message : fileError ?? 'Unknown file read error');
|
|
266
|
+
let message = String(fileError instanceof Error ? fileError.message : (fileError ?? 'Unknown file read error'));
|
|
235
267
|
this.warn(`Error reading file ${fullPath}: ${message}`);
|
|
236
268
|
}
|
|
237
269
|
}
|
|
@@ -243,7 +275,7 @@ export default function constSysfsExpr(options: EmbedPluginOptions = {}): SysfsP
|
|
|
243
275
|
hasReplaced = true;
|
|
244
276
|
count++;
|
|
245
277
|
} catch (error: unknown) {
|
|
246
|
-
|
|
278
|
+
const message = String(error instanceof Error ? error.message : (error ?? 'Unknown error during file processing'));
|
|
247
279
|
this.error(`Could not process files for constSysfsExpr: ${message}`, node.loc?.start.index);
|
|
248
280
|
return;
|
|
249
281
|
}
|
|
@@ -251,11 +283,25 @@ export default function constSysfsExpr(options: EmbedPluginOptions = {}): SysfsP
|
|
|
251
283
|
},
|
|
252
284
|
});
|
|
253
285
|
} catch (error: unknown) {
|
|
254
|
-
const message = String(error instanceof Error ? error.message : error ?? 'Unknown parsing error');
|
|
286
|
+
const message = String(error instanceof Error ? error.message : (error ?? 'Unknown parsing error'));
|
|
255
287
|
this.error(`Failed to parse ${id}: ${message}`);
|
|
256
288
|
return null;
|
|
257
289
|
}
|
|
258
290
|
|
|
291
|
+
if (constSysfsImport !== null && hasReplaced) {
|
|
292
|
+
const info = constSysfsImport as ImportSpecifierInfo;
|
|
293
|
+
if (info.isOnlySpecifier) {
|
|
294
|
+
let endPos = info.declEnd;
|
|
295
|
+
if (code[endPos] === '\n') endPos++;
|
|
296
|
+
else if (code[endPos] === '\r' && code[endPos + 1] === '\n') endPos += 2;
|
|
297
|
+
magicString.remove(info.declStart, endPos);
|
|
298
|
+
} else if (info.nextSpecifierStart !== null) {
|
|
299
|
+
magicString.remove(info.specifierStart, info.nextSpecifierStart);
|
|
300
|
+
} else if (info.prevSpecifierEnd !== null) {
|
|
301
|
+
magicString.remove(info.prevSpecifierEnd, info.specifierEnd);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
259
305
|
// If no replacements were made, return null
|
|
260
306
|
if (!hasReplaced) {
|
|
261
307
|
return null;
|
package/src/transpiler.ts
CHANGED
|
@@ -7,7 +7,8 @@ import terser from '@rollup/plugin-terser';
|
|
|
7
7
|
import typescript from '@rollup/plugin-typescript';
|
|
8
8
|
import url from '@rollup/plugin-url';
|
|
9
9
|
import nodePolyfills from 'rollup-plugin-polyfill-node';
|
|
10
|
-
import
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { InputPluginOption, OutputBundle, OutputOptions, Plugin, RollupOptions, rollup, watch as rollupWatch } from 'rollup';
|
|
11
12
|
import { minify_sync } from 'terser';
|
|
12
13
|
import scss from 'rollup-plugin-scss';
|
|
13
14
|
import * as sass from 'sass';
|
|
@@ -16,8 +17,10 @@ import path from 'path';
|
|
|
16
17
|
import { pathToFileURL } from 'url';
|
|
17
18
|
import dotenv from 'dotenv';
|
|
18
19
|
import injectProcessEnv from 'rollup-plugin-inject-process-env';
|
|
20
|
+
import { performance } from 'perf_hooks';
|
|
19
21
|
import { ExecutePluginModule, InitializePlugins } from './plugin-api';
|
|
20
22
|
import { Logger } from './logger';
|
|
23
|
+
import { PluginJson } from './plugin-json';
|
|
21
24
|
import constSysfsExpr from './static-embed';
|
|
22
25
|
|
|
23
26
|
const env = dotenv.config().parsed ?? {};
|
|
@@ -41,6 +44,8 @@ declare const __call_server_method__: (methodName: string, kwargs: any) => any;
|
|
|
41
44
|
export interface TranspilerProps {
|
|
42
45
|
minify: boolean;
|
|
43
46
|
pluginName: string;
|
|
47
|
+
watch?: boolean;
|
|
48
|
+
isMillennium?: boolean;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
enum BuildTarget {
|
|
@@ -129,6 +134,8 @@ function stripPluginPrefix(message: string): string {
|
|
|
129
134
|
return message;
|
|
130
135
|
}
|
|
131
136
|
|
|
137
|
+
class BuildFailedError extends Error {}
|
|
138
|
+
|
|
132
139
|
abstract class MillenniumBuild {
|
|
133
140
|
protected abstract readonly externals: ReadonlySet<string>;
|
|
134
141
|
protected abstract readonly forbidden: ReadonlyMap<string, string>;
|
|
@@ -144,6 +151,25 @@ abstract class MillenniumBuild {
|
|
|
144
151
|
return this.externals.has(id);
|
|
145
152
|
}
|
|
146
153
|
|
|
154
|
+
async watchConfig(input: string, sysfsPlugin: InputPluginOption, isMillennium: boolean): Promise<RollupOptions> {
|
|
155
|
+
return {
|
|
156
|
+
input,
|
|
157
|
+
plugins: await this.plugins(sysfsPlugin),
|
|
158
|
+
onwarn: (warning) => {
|
|
159
|
+
const msg = stripPluginPrefix(warning.message);
|
|
160
|
+
const loc = logLocation(warning);
|
|
161
|
+
if (warning.plugin === 'typescript') {
|
|
162
|
+
Logger.error(msg, loc);
|
|
163
|
+
} else {
|
|
164
|
+
Logger.warn(msg, loc);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
context: 'window',
|
|
168
|
+
external: (id) => this.isExternal(id),
|
|
169
|
+
output: this.output(isMillennium),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
147
173
|
async build(input: string, sysfsPlugin: InputPluginOption, isMillennium: boolean): Promise<void> {
|
|
148
174
|
let hasErrors = false;
|
|
149
175
|
|
|
@@ -167,7 +193,7 @@ abstract class MillenniumBuild {
|
|
|
167
193
|
|
|
168
194
|
await (await rollup(config)).write(config.output as OutputOptions);
|
|
169
195
|
|
|
170
|
-
if (hasErrors)
|
|
196
|
+
if (hasErrors) throw new BuildFailedError();
|
|
171
197
|
}
|
|
172
198
|
}
|
|
173
199
|
|
|
@@ -273,10 +299,50 @@ class WebkitBuild extends MillenniumBuild {
|
|
|
273
299
|
}
|
|
274
300
|
}
|
|
275
301
|
|
|
276
|
-
|
|
302
|
+
function RunWatchMode(frontendConfig: RollupOptions, webkitConfig: RollupOptions | null): void {
|
|
303
|
+
const configs = webkitConfig ? [frontendConfig, webkitConfig] : [frontendConfig];
|
|
304
|
+
const watcher = rollupWatch(configs);
|
|
305
|
+
|
|
306
|
+
console.log(chalk.blueBright.bold('watch'), 'watching for file changes...');
|
|
307
|
+
|
|
308
|
+
watcher.on('event', async (event) => {
|
|
309
|
+
if (event.code === 'BUNDLE_START') {
|
|
310
|
+
const label = (event.output as readonly string[]).some((f) => f.includes('index.js')) ? 'frontend' : 'webkit';
|
|
311
|
+
console.log(chalk.yellowBright.bold('watch'), `rebuilding ${label}...`);
|
|
312
|
+
} else if (event.code === 'BUNDLE_END') {
|
|
313
|
+
const label = (event.output as readonly string[]).some((f) => f.includes('index.js')) ? 'frontend' : 'webkit';
|
|
314
|
+
console.log(chalk.greenBright.bold('watch'), `${label} built in ${chalk.green(`${event.duration}ms`)}`);
|
|
315
|
+
await event.result.close();
|
|
316
|
+
} else if (event.code === 'ERROR') {
|
|
317
|
+
const err = event.error;
|
|
318
|
+
const msg = stripPluginPrefix(err?.message ?? String(err));
|
|
319
|
+
Logger.error(msg, logLocation(err as any));
|
|
320
|
+
if (event.result) await event.result.close();
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const shutdown = () => {
|
|
325
|
+
console.log(chalk.yellowBright.bold('watch'), 'stopping...');
|
|
326
|
+
watcher.close();
|
|
327
|
+
process.exit(0);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
process.on('SIGINT', shutdown);
|
|
331
|
+
process.on('SIGUSR2', shutdown);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export const TranspilerPluginComponent = async (pluginJson: PluginJson, props: TranspilerProps) => {
|
|
277
335
|
const webkitDir = './webkit/index.tsx';
|
|
278
336
|
const frontendDir = getFrontendDir(pluginJson);
|
|
279
337
|
const sysfs = constSysfsExpr();
|
|
338
|
+
const isMillennium = props.isMillennium ?? false;
|
|
339
|
+
|
|
340
|
+
if (props.watch) {
|
|
341
|
+
const frontendConfig = await new FrontendBuild(frontendDir, props).watchConfig(resolveEntryFile(frontendDir), sysfs.plugin, isMillennium);
|
|
342
|
+
const webkitConfig = fs.existsSync(webkitDir) ? await new WebkitBuild(props).watchConfig(webkitDir, sysfs.plugin, isMillennium) : null;
|
|
343
|
+
RunWatchMode(frontendConfig, webkitConfig);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
280
346
|
|
|
281
347
|
try {
|
|
282
348
|
await new FrontendBuild(frontendDir, props).build(resolveEntryFile(frontendDir), sysfs.plugin, isMillennium);
|
|
@@ -292,7 +358,10 @@ export const TranspilerPluginComponent = async (isMillennium: boolean, pluginJso
|
|
|
292
358
|
envCount: Object.keys(env).length || undefined,
|
|
293
359
|
});
|
|
294
360
|
} catch (exception: any) {
|
|
295
|
-
|
|
361
|
+
if (!(exception instanceof BuildFailedError)) {
|
|
362
|
+
Logger.error(stripPluginPrefix(exception?.message ?? String(exception)), logLocation(exception));
|
|
363
|
+
}
|
|
364
|
+
Logger.failed({ elapsedMs: performance.now() - global.PerfStartTime, buildType: props.minify ? 'prod' : 'dev' });
|
|
296
365
|
process.exit(1);
|
|
297
366
|
}
|
|
298
367
|
};
|