@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.
@@ -3,7 +3,8 @@
3
3
  "allow": [
4
4
  "Bash(npm show:*)",
5
5
  "Bash(bun remove:*)",
6
- "Bash(bun run:*)"
6
+ "Bash(bun run:*)",
7
+ "Bash(node:*)"
7
8
  ]
8
9
  }
9
10
  }
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: 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
- process.exit(1);
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
- const TranspilerPluginComponent = async (isMillennium, pluginJson, props) => {
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
- Logger.error(stripPluginPrefix(exception?.message ?? String(exception)), logLocation(exception));
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?.name,
829
+ pluginName: json.name,
830
+ watch: parameters.watch || false,
831
+ isMillennium: bIsMillennium,
721
832
  };
722
- TranspilerPluginComponent(bIsMillennium, json, props);
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$1.now();
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.0.1",
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": {
@@ -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<any> => {
6
- return new Promise<any>((resolve, reject) => {
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: any) => {
29
+ .then((json: PluginJson) => {
29
30
  const props: TranspilerProps = {
30
31
  minify: bTersePlugin,
31
- pluginName: json?.name,
32
+ pluginName: json.name,
33
+ watch: parameters.watch || false,
34
+ isMillennium: bIsMillennium,
32
35
  };
33
36
 
34
- TranspilerPluginComponent(bIsMillennium, json, props);
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
+ }
@@ -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: isMillennium,
88
+ isMillennium,
89
+ watch,
82
90
  };
83
91
  };
@@ -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
- ? path.dirname(pathOrPattern)
187
- : path.resolve(path.dirname(id), path.dirname(pathOrPattern));
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
- const message = String(error instanceof Error ? error.message : error ?? 'Unknown error during file processing');
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 { InputPluginOption, OutputBundle, OutputOptions, Plugin, RollupOptions, rollup } from 'rollup';
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) process.exit(1);
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
- export const TranspilerPluginComponent = async (isMillennium: boolean, pluginJson: any, props: TranspilerProps) => {
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
- Logger.error(stripPluginPrefix(exception?.message ?? String(exception)), logLocation(exception));
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
  };