@steambrew/ttc 2.8.7 → 3.0.1

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/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import chalk from 'chalk';
3
+ import fs, { readFileSync, existsSync, readFile as readFile$1 } from 'fs';
3
4
  import path, { dirname } from 'path';
4
- import { fileURLToPath } from 'url';
5
- import { readFile } from 'fs/promises';
6
- import fs, { existsSync, readFile as readFile$1 } from 'fs';
5
+ import { fileURLToPath, pathToFileURL } from 'url';
6
+ import { readFile, access } from 'fs/promises';
7
7
  import babel from '@rollup/plugin-babel';
8
8
  import commonjs from '@rollup/plugin-commonjs';
9
9
  import json from '@rollup/plugin-json';
@@ -12,8 +12,8 @@ import replace from '@rollup/plugin-replace';
12
12
  import terser from '@rollup/plugin-terser';
13
13
  import typescript from '@rollup/plugin-typescript';
14
14
  import url from '@rollup/plugin-url';
15
- import { rollup } from 'rollup';
16
15
  import nodePolyfills from 'rollup-plugin-polyfill-node';
16
+ import { 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';
@@ -26,57 +26,53 @@ import MagicString from 'magic-string';
26
26
  import _traverse from '@babel/traverse';
27
27
  import { performance as performance$1 } from 'perf_hooks';
28
28
 
29
+ const version = JSON.parse(readFileSync(path.resolve(dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf8')).version;
29
30
  const Logger = {
30
- Info: (name, ...LogMessage) => {
31
- console.log(chalk.magenta.bold(name), ...LogMessage);
31
+ warn(message, loc) {
32
+ if (loc) {
33
+ console.warn(`${chalk.dim(loc + ':')} ${message}`);
34
+ }
35
+ else {
36
+ console.warn(`${chalk.yellow('warning:')} ${message}`);
37
+ }
32
38
  },
33
- Warn: (...LogMessage) => {
34
- console.log(chalk.yellow.bold('**'), ...LogMessage);
39
+ error(message, loc) {
40
+ if (loc) {
41
+ console.error(`${chalk.red(loc + ':')} ${message}`);
42
+ }
43
+ else {
44
+ console.error(`${chalk.red('error:')} ${message}`);
45
+ }
35
46
  },
36
- Error: (...LogMessage) => {
37
- console.error(chalk.red.bold('!!'), ...LogMessage);
47
+ update(current, latest, installCmd) {
48
+ console.log(`\n${chalk.yellow('update available')} ${chalk.dim(current)} → ${chalk.green(latest)}`);
49
+ console.log(`${chalk.dim('run:')} ${installCmd}\n`);
38
50
  },
39
- Tree: (name, strTitle, LogObject) => {
40
- const fixedPadding = 15; // <-- always pad keys to 15 characters
41
- console.log(chalk.greenBright.bold(name).padEnd(fixedPadding), strTitle);
42
- const isLocalPath = (strTestPath) => {
43
- const filePathRegex = /^(\/|\.\/|\.\.\/|\w:\/)?([\w-.]+\/)*[\w-.]+\.\w+$/;
44
- return filePathRegex.test(strTestPath);
45
- };
46
- for (const [key, value] of Object.entries(LogObject)) {
47
- let color = chalk.white;
48
- switch (typeof value) {
49
- case 'string':
50
- color = isLocalPath(value) ? chalk.blueBright : chalk.white;
51
- break;
52
- case 'boolean':
53
- color = chalk.green;
54
- break;
55
- case 'number':
56
- color = chalk.yellow;
57
- break;
58
- }
59
- console.log(chalk.greenBright.bold(`${key}: `).padEnd(fixedPadding), color(String(value)));
60
- }
51
+ done({ elapsedMs, buildType, sysfsCount, envCount }) {
52
+ const elapsed = `${(elapsedMs / 1000).toFixed(2)}s`;
53
+ const meta = [`ttc v${version}`];
54
+ if (buildType === 'dev')
55
+ meta.push('no type checking');
56
+ if (sysfsCount)
57
+ meta.push(`${sysfsCount} constSysfsExpr`);
58
+ if (envCount)
59
+ meta.push(`${envCount} env var${envCount > 1 ? 's' : ''}`);
60
+ console.log(`${chalk.green('Finished')} ${buildType} in ${elapsed} ` + chalk.dim('(' + meta.join(', ') + ')'));
61
61
  },
62
62
  };
63
63
 
64
- /***
65
- * @brief print the parameter list to the stdout
66
- */
67
64
  const PrintParamHelp = () => {
68
- console.log('millennium-ttc parameter list:' +
69
- '\n\t' +
70
- chalk.magenta('--help') +
71
- ': display parameter list' +
72
- '\n\t' +
73
- chalk.bold.red('--build') +
74
- ': ' +
75
- chalk.bold.red('(required)') +
76
- ': build type [dev, prod] (prod minifies code)' +
77
- '\n\t' +
78
- chalk.magenta('--target') +
79
- ': path to plugin, default to cwd');
65
+ console.log([
66
+ '',
67
+ 'usage: millennium-ttc --build <dev|prod> [options]',
68
+ '',
69
+ 'options:',
70
+ ' --build <dev|prod> build type (prod enables minification)',
71
+ ' --target <path> plugin directory (default: current directory)',
72
+ ' --no-update skip update check',
73
+ ' --help show this message',
74
+ '',
75
+ ].join('\n'));
80
76
  };
81
77
  var BuildType;
82
78
  (function (BuildType) {
@@ -89,9 +85,8 @@ const ValidateParameters = (args) => {
89
85
  PrintParamHelp();
90
86
  process.exit();
91
87
  }
92
- // startup args are invalid
93
88
  if (!args.includes('--build')) {
94
- Logger.Error('Received invalid arguments...');
89
+ Logger.error('missing required argument: --build');
95
90
  PrintParamHelp();
96
91
  process.exit();
97
92
  }
@@ -106,14 +101,14 @@ const ValidateParameters = (args) => {
106
101
  typeProp = BuildType.ProdBuild;
107
102
  break;
108
103
  default: {
109
- Logger.Error('--build parameter must be preceded by build type [dev, prod]');
104
+ Logger.error('--build must be one of: dev, prod');
110
105
  process.exit();
111
106
  }
112
107
  }
113
108
  }
114
109
  if (args[i] == '--target') {
115
110
  if (args[i + 1] === undefined) {
116
- Logger.Error('--target parameter must be preceded by system path');
111
+ Logger.error('--target requires a path argument');
117
112
  process.exit();
118
113
  }
119
114
  targetProp = args[i + 1];
@@ -129,40 +124,61 @@ const ValidateParameters = (args) => {
129
124
  };
130
125
  };
131
126
 
127
+ async function fileExists(filePath) {
128
+ return access(filePath).then(() => true).catch(() => false);
129
+ }
130
+ async function detectPackageManager() {
131
+ const userAgent = process.env.npm_config_user_agent ?? '';
132
+ if (userAgent.startsWith('bun'))
133
+ return 'bun';
134
+ if (userAgent.startsWith('pnpm'))
135
+ return 'pnpm';
136
+ if (userAgent.startsWith('yarn'))
137
+ return 'yarn';
138
+ const cwd = process.cwd();
139
+ if (await fileExists(path.join(cwd, 'bun.lock')) || await fileExists(path.join(cwd, 'bun.lockb')))
140
+ return 'bun';
141
+ if (await fileExists(path.join(cwd, 'pnpm-lock.yaml')))
142
+ return 'pnpm';
143
+ if (await fileExists(path.join(cwd, 'yarn.lock')))
144
+ return 'yarn';
145
+ return 'npm';
146
+ }
147
+ function installCommand(pm, pkg) {
148
+ switch (pm) {
149
+ case 'bun': return `bun add -d ${pkg}`;
150
+ case 'pnpm': return `pnpm add -D ${pkg}`;
151
+ case 'yarn': return `yarn add -D ${pkg}`;
152
+ default: return `npm i -D ${pkg}`;
153
+ }
154
+ }
132
155
  const CheckForUpdates = async () => {
133
- return new Promise(async (resolve) => {
156
+ try {
134
157
  const packageJsonPath = path.resolve(dirname(fileURLToPath(import.meta.url)), '../package.json');
135
158
  const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
136
- fetch('https://registry.npmjs.org/@steambrew/ttc')
137
- .then((response) => response.json())
138
- .then((json) => {
139
- if (json?.['dist-tags']?.latest != packageJson.version) {
140
- Logger.Tree('versionMon', `@steambrew/ttc@${packageJson.version} requires update to ${json?.['dist-tags']?.latest}`, {
141
- cmd: `run "npm install @steambrew/ttc@${json?.['dist-tags']?.latest}" to get latest updates!`,
142
- });
143
- resolve(true);
144
- }
145
- else {
146
- Logger.Info('versionMon', `@steambrew/ttc@${packageJson.version} is up-to-date!`);
147
- resolve(false);
148
- }
149
- })
150
- .catch((exception) => {
151
- Logger.Error('Failed to check for updates: ' + exception);
152
- resolve(false);
153
- });
154
- });
159
+ const response = await fetch('https://registry.npmjs.org/@steambrew/ttc');
160
+ const json = await response.json();
161
+ const latest = json?.['dist-tags']?.latest;
162
+ if (latest && latest !== packageJson.version) {
163
+ const pm = await detectPackageManager();
164
+ Logger.update(packageJson.version, latest, installCommand(pm, `@steambrew/ttc@${latest}`));
165
+ return true;
166
+ }
167
+ }
168
+ catch {
169
+ // network or parse failure — silently skip
170
+ }
171
+ return false;
155
172
  };
156
173
 
157
174
  const ValidatePlugin = (bIsMillennium, target) => {
158
175
  return new Promise((resolve, reject) => {
159
176
  if (!existsSync(target)) {
160
- console.error(chalk.red.bold(`\n[-] --target [${target}] `) + chalk.red('is not a valid system path'));
177
+ Logger.error(`target path does not exist: ${target}`);
161
178
  reject();
162
179
  return;
163
180
  }
164
181
  if (bIsMillennium) {
165
- console.log(chalk.green.bold('\n[+] Using Millennium internal build configuration'));
166
182
  resolve({
167
183
  name: 'core',
168
184
  common_name: 'Millennium',
@@ -174,27 +190,28 @@ const ValidatePlugin = (bIsMillennium, target) => {
174
190
  }
175
191
  const pluginModule = path.join(target, 'plugin.json');
176
192
  if (!existsSync(pluginModule)) {
177
- console.error(chalk.red.bold(`\n[-] --target [${target}] `) + chalk.red('is not a valid plugin (missing plugin.json)'));
193
+ Logger.error(`plugin.json not found: ${pluginModule}`);
178
194
  reject();
179
195
  return;
180
196
  }
181
197
  readFile$1(pluginModule, 'utf8', (err, data) => {
182
198
  if (err) {
183
- console.error(chalk.red.bold(`\n[-] couldn't read plugin.json from [${pluginModule}]`));
199
+ Logger.error(`could not read plugin.json: ${pluginModule}`);
184
200
  reject();
185
201
  return;
186
202
  }
187
203
  try {
188
- if (!('name' in JSON.parse(data))) {
189
- console.error(chalk.red.bold(`\n[-] target plugin doesn't contain "name" in plugin.json [${pluginModule}]`));
204
+ const parsed = JSON.parse(data);
205
+ if (!('name' in parsed)) {
206
+ Logger.error('plugin.json is missing required "name" field');
190
207
  reject();
191
208
  }
192
209
  else {
193
- resolve(JSON.parse(data));
210
+ resolve(parsed);
194
211
  }
195
212
  }
196
213
  catch (parseError) {
197
- console.error(chalk.red.bold(`\n[-] couldn't parse JSON in plugin.json from [${pluginModule}]`));
214
+ Logger.error('plugin.json contains invalid JSON:', pluginModule);
198
215
  reject();
199
216
  }
200
217
  });
@@ -205,178 +222,42 @@ const ValidatePlugin = (bIsMillennium, target) => {
205
222
  * @description Append the active plugin to the global plugin
206
223
  * list and notify that the frontend Loaded.
207
224
  */
208
- function ExecutePluginModule() {
209
- let MillenniumStore = window.MILLENNIUM_PLUGIN_SETTINGS_STORE[pluginName];
210
- function OnPluginConfigChange(key, __, value) {
211
- if (key in MillenniumStore.settingsStore) {
212
- MillenniumStore.ignoreProxyFlag = true;
213
- MillenniumStore.settingsStore[key] = value;
214
- MillenniumStore.ignoreProxyFlag = false;
215
- }
216
- }
217
- /** Expose the OnPluginConfigChange so it can be called externally */
218
- MillenniumStore.OnPluginConfigChange = OnPluginConfigChange;
219
- MILLENNIUM_BACKEND_IPC.postMessage(0, { pluginName: pluginName, methodName: '__builtins__.__millennium_plugin_settings_parser__' }).then(async (response) => {
220
- /**
221
- * __millennium_plugin_settings_parser__ will return false if the plugin has no settings.
222
- * If the plugin has settings, it will return a base64 encoded string.
223
- * The string is then decoded and parsed into an object.
224
- */
225
- if (typeof response.returnValue === 'string') {
226
- MillenniumStore.ignoreProxyFlag = true;
227
- /** Initialize the settings store from the settings returned from the backend. */
228
- MillenniumStore.settingsStore = MillenniumStore.DefinePluginSetting(Object.fromEntries(JSON.parse(atob(response.returnValue)).map((item) => [item.functionName, item])));
229
- MillenniumStore.ignoreProxyFlag = false;
230
- }
231
- /** @ts-ignore: call the plugin main after the settings have been parsed. This prevent plugin settings from being undefined at top level. */
232
- let PluginModule = PluginEntryPointMain();
233
- /** Assign the plugin on plugin list. */
234
- Object.assign(window.PLUGIN_LIST[pluginName], {
235
- ...PluginModule,
236
- __millennium_internal_plugin_name_do_not_use_or_change__: pluginName,
237
- });
238
- /** Run the rolled up plugins default exported function */
239
- let pluginProps = await PluginModule.default();
240
- function isValidSidebarNavComponent(obj) {
241
- return obj && obj.title !== undefined && obj.icon !== undefined && obj.content !== undefined;
242
- }
243
- if (pluginProps && isValidSidebarNavComponent(pluginProps)) {
244
- window.MILLENNIUM_SIDEBAR_NAVIGATION_PANELS[pluginName] = pluginProps;
245
- }
246
- else {
247
- console.warn(`Plugin ${pluginName} does not contain proper SidebarNavigation props and therefor can't be mounted by Millennium. Please ensure it has a title, icon, and content.`);
248
- return;
249
- }
250
- /** If the current module is a client module, post message id=1 which calls the front_end_loaded method on the backend. */
251
- if (MILLENNIUM_IS_CLIENT_MODULE) {
252
- MILLENNIUM_BACKEND_IPC.postMessage(1, { pluginName: pluginName });
253
- }
225
+ async function ExecutePluginModule() {
226
+ let PluginModule = PluginEntryPointMain();
227
+ /** Assign the plugin on plugin list. */
228
+ Object.assign(window.PLUGIN_LIST[pluginName], {
229
+ ...PluginModule,
230
+ __millennium_internal_plugin_name_do_not_use_or_change__: pluginName,
254
231
  });
232
+ /** Run the rolled up plugins default exported function */
233
+ let pluginProps = await PluginModule.default();
234
+ function isValidSidebarNavComponent(obj) {
235
+ return obj && obj.title !== undefined && obj.icon !== undefined && obj.content !== undefined;
236
+ }
237
+ if (pluginProps && isValidSidebarNavComponent(pluginProps)) {
238
+ window.MILLENNIUM_SIDEBAR_NAVIGATION_PANELS[pluginName] = pluginProps;
239
+ }
240
+ /** If the current module is a client module, post message id=1 which calls the front_end_loaded method on the backend. */
241
+ if (MILLENNIUM_IS_CLIENT_MODULE) {
242
+ MILLENNIUM_BACKEND_IPC.postMessage(1, { pluginName: pluginName });
243
+ }
255
244
  }
256
245
  /**
257
246
  * @description Initialize the plugins settings store and the plugin list.
258
247
  * This function is called once per plugin and is used to store the plugin settings and the plugin list.
259
248
  */
260
249
  function InitializePlugins() {
261
- var _a, _b;
262
- /**
263
- * This function is called n times depending on n plugin count,
264
- * Create the plugin list if it wasn't already created
265
- */
250
+ var _a;
266
251
  (_a = (window.PLUGIN_LIST || (window.PLUGIN_LIST = {})))[pluginName] || (_a[pluginName] = {});
267
- (_b = (window.MILLENNIUM_PLUGIN_SETTINGS_STORE || (window.MILLENNIUM_PLUGIN_SETTINGS_STORE = {})))[pluginName] || (_b[pluginName] = {});
268
252
  window.MILLENNIUM_SIDEBAR_NAVIGATION_PANELS || (window.MILLENNIUM_SIDEBAR_NAVIGATION_PANELS = {});
269
- /**
270
- * Accepted IPC message types from Millennium backend.
271
- */
272
- let IPCType;
273
- (function (IPCType) {
274
- IPCType[IPCType["CallServerMethod"] = 0] = "CallServerMethod";
275
- })(IPCType || (IPCType = {}));
276
- let MillenniumStore = window.MILLENNIUM_PLUGIN_SETTINGS_STORE[pluginName];
277
- let IPCMessageId = `Millennium.Internal.IPC.[${pluginName}]`;
278
- let isClientModule = MILLENNIUM_IS_CLIENT_MODULE;
279
- const ComponentTypeMap = {
280
- DropDown: ['string', 'number', 'boolean'],
281
- NumberTextInput: ['number'],
282
- StringTextInput: ['string'],
283
- FloatTextInput: ['number'],
284
- CheckBox: ['boolean'],
285
- NumberSlider: ['number'],
286
- FloatSlider: ['number'],
287
- };
288
- MillenniumStore.ignoreProxyFlag = false;
289
- function DelegateToBackend(pluginName, name, value) {
290
- return MILLENNIUM_BACKEND_IPC.postMessage(IPCType.CallServerMethod, {
291
- pluginName,
292
- methodName: '__builtins__.__update_settings_value__',
293
- argumentList: { name, value },
294
- });
295
- }
296
- async function ClientInitializeIPC() {
297
- /** Wait for the MainWindowBrowser to not be undefined */
298
- while (typeof MainWindowBrowserManager === 'undefined') {
299
- await new Promise((resolve) => setTimeout(resolve, 0));
300
- }
301
- MainWindowBrowserManager?.m_browser?.on('message', (messageId, data) => {
302
- if (messageId !== IPCMessageId) {
303
- return;
304
- }
305
- const { name, value } = JSON.parse(data);
306
- MillenniumStore.ignoreProxyFlag = true;
307
- MillenniumStore.settingsStore[name] = value;
308
- DelegateToBackend(pluginName, name, value);
309
- MillenniumStore.ignoreProxyFlag = false;
310
- });
311
- }
312
- if (isClientModule) {
313
- ClientInitializeIPC();
314
- }
315
- const StartSettingPropagation = (name, value) => {
316
- if (MillenniumStore.ignoreProxyFlag) {
317
- return;
318
- }
319
- if (isClientModule) {
320
- DelegateToBackend(pluginName, name, value);
321
- /** If the browser doesn't exist yet, no use sending anything to it. */
322
- if (typeof MainWindowBrowserManager !== 'undefined') {
323
- MainWindowBrowserManager?.m_browser?.PostMessage(IPCMessageId, JSON.stringify({ name, value }));
324
- }
325
- }
326
- else {
327
- /** Send the message to the SharedJSContext */
328
- SteamClient.BrowserView.PostMessageToParent(IPCMessageId, JSON.stringify({ name, value }));
329
- }
330
- };
331
- function clamp(value, min, max) {
332
- return Math.max(min, Math.min(max, value));
333
- }
334
- const DefinePluginSetting = (obj) => {
335
- return new Proxy(obj, {
336
- set(target, property, value) {
337
- if (!(property in target)) {
338
- throw new TypeError(`Property ${String(property)} does not exist on plugin settings`);
339
- }
340
- const settingType = ComponentTypeMap[target[property].type];
341
- const range = target[property]?.range;
342
- /** Clamp the value between the given range */
343
- if (settingType.includes('number') && typeof value === 'number') {
344
- if (range) {
345
- value = clamp(value, range[0], range[1]);
346
- }
347
- value || (value = 0); // Fallback to 0 if the value is undefined or null
348
- }
349
- /** Check if the value is of the proper type */
350
- if (!settingType.includes(typeof value)) {
351
- throw new TypeError(`Expected ${settingType.join(' or ')}, got ${typeof value}`);
352
- }
353
- target[property].value = value;
354
- StartSettingPropagation(String(property), value);
355
- return true;
356
- },
357
- get(target, property) {
358
- if (property === '__raw_get_internals__') {
359
- return target;
360
- }
361
- if (property in target) {
362
- return target[property].value;
363
- }
364
- return undefined;
365
- },
366
- });
367
- };
368
- MillenniumStore.DefinePluginSetting = DefinePluginSetting;
369
- MillenniumStore.settingsStore = DefinePluginSetting({});
370
253
  }
371
254
 
372
255
  const traverse = _traverse.default;
373
- const Log = (...message) => {
374
- console.log(chalk.blueBright.bold('constSysfsExpr'), ...message);
375
- };
376
256
  function constSysfsExpr(options = {}) {
377
257
  const filter = createFilter(options.include, options.exclude);
378
258
  const pluginName = 'millennium-const-sysfs-expr';
379
- return {
259
+ let count = 0;
260
+ const plugin = {
380
261
  name: pluginName,
381
262
  transform(code, id) {
382
263
  if (!filter(id))
@@ -512,7 +393,6 @@ function constSysfsExpr(options = {}) {
512
393
  return;
513
394
  }
514
395
  try {
515
- const currentLocString = node.loc?.start ? ` at ${id}:${node.loc.start.line}:${node.loc.start.column}` : ` in ${id}`;
516
396
  const searchBasePath = callOptions.basePath
517
397
  ? path.isAbsolute(callOptions.basePath)
518
398
  ? callOptions.basePath
@@ -521,13 +401,11 @@ function constSysfsExpr(options = {}) {
521
401
  ? path.dirname(pathOrPattern)
522
402
  : path.resolve(path.dirname(id), path.dirname(pathOrPattern));
523
403
  let embeddedContent;
524
- let embeddedCount = 0;
525
404
  const isPotentialPattern = /[?*+!@()[\]{}]/.test(pathOrPattern);
526
405
  if (!isPotentialPattern &&
527
406
  fs.existsSync(path.resolve(searchBasePath, pathOrPattern)) &&
528
407
  fs.statSync(path.resolve(searchBasePath, pathOrPattern)).isFile()) {
529
408
  const singleFilePath = path.resolve(searchBasePath, pathOrPattern);
530
- Log(`Mode: Single file (first argument "${pathOrPattern}" resolved to "${singleFilePath}" relative to "${searchBasePath}")`);
531
409
  try {
532
410
  const rawContent = fs.readFileSync(singleFilePath, callOptions.encoding);
533
411
  const contentString = rawContent.toString();
@@ -537,8 +415,6 @@ function constSysfsExpr(options = {}) {
537
415
  fileName: path.relative(searchBasePath, singleFilePath),
538
416
  };
539
417
  embeddedContent = JSON.stringify(fileInfo);
540
- embeddedCount = 1;
541
- Log(`Embedded 1 specific file for call${currentLocString}`);
542
418
  }
543
419
  catch (fileError) {
544
420
  let message = String(fileError instanceof Error ? fileError.message : fileError ?? 'Unknown file read error');
@@ -547,8 +423,6 @@ function constSysfsExpr(options = {}) {
547
423
  }
548
424
  }
549
425
  else {
550
- Log(`Mode: Multi-file (first argument "${pathOrPattern}" is pattern or not a single file)`);
551
- Log(`Searching with pattern "${pathOrPattern}" in directory "${searchBasePath}" (encoding: ${callOptions.encoding})`);
552
426
  const matchingFiles = glob.sync(pathOrPattern, {
553
427
  cwd: searchBasePath,
554
428
  nodir: true,
@@ -571,15 +445,13 @@ function constSysfsExpr(options = {}) {
571
445
  }
572
446
  }
573
447
  embeddedContent = JSON.stringify(fileInfoArray);
574
- embeddedCount = fileInfoArray.length;
575
- Log(`Embedded ${embeddedCount} file(s) matching pattern for call${currentLocString}`);
576
448
  }
577
449
  // Replace the call expression with the generated content string
578
450
  magicString.overwrite(node.start, node.end, embeddedContent);
579
451
  hasReplaced = true;
452
+ count++;
580
453
  }
581
454
  catch (error) {
582
- console.error(`Failed to process files for constSysfsExpr call in ${id}:`, error);
583
455
  const message = String(error instanceof Error ? error.message : error ?? 'Unknown error during file processing');
584
456
  this.error(`Could not process files for constSysfsExpr: ${message}`, node.loc?.start.index);
585
457
  return;
@@ -589,7 +461,6 @@ function constSysfsExpr(options = {}) {
589
461
  });
590
462
  }
591
463
  catch (error) {
592
- console.error(`Error parsing or traversing ${id}:`, error);
593
464
  const message = String(error instanceof Error ? error.message : error ?? 'Unknown parsing error');
594
465
  this.error(`Failed to parse ${id}: ${message}`);
595
466
  return null;
@@ -606,207 +477,155 @@ function constSysfsExpr(options = {}) {
606
477
  return result;
607
478
  },
608
479
  };
480
+ return { plugin, getCount: () => count };
609
481
  }
610
482
 
611
- const envConfig = dotenv.config().parsed || {};
612
- if (envConfig) {
613
- Logger.Info('envVars', 'Processing ' + Object.keys(envConfig).length + ' environment variables... ' + chalk.green.bold('okay'));
614
- }
615
- const envVars = Object.keys(envConfig).reduce((acc, key) => {
616
- acc[key] = envConfig[key];
617
- return acc;
618
- }, {});
619
- var ComponentType;
620
- (function (ComponentType) {
621
- ComponentType[ComponentType["Plugin"] = 0] = "Plugin";
622
- ComponentType[ComponentType["Webkit"] = 1] = "Webkit";
623
- })(ComponentType || (ComponentType = {}));
624
- const WrappedCallServerMethod = 'const __call_server_method__ = (methodName, kwargs) => Millennium.callServerMethod(pluginName, methodName, kwargs)';
483
+ const env = dotenv.config().parsed ?? {};
484
+ var BuildTarget;
485
+ (function (BuildTarget) {
486
+ BuildTarget[BuildTarget["Plugin"] = 0] = "Plugin";
487
+ BuildTarget[BuildTarget["Webkit"] = 1] = "Webkit";
488
+ })(BuildTarget || (BuildTarget = {}));
489
+ const kCallServerMethod = 'const __call_server_method__ = (methodName, kwargs) => Millennium.callServerMethod(pluginName, methodName, kwargs)';
625
490
  function __wrapped_callable__(route) {
626
491
  if (route.startsWith('webkit:')) {
627
492
  return MILLENNIUM_API.callable((methodName, kwargs) => MILLENNIUM_API.__INTERNAL_CALL_WEBKIT_METHOD__(pluginName, methodName, kwargs), route.replace(/^webkit:/, ''));
628
493
  }
629
494
  return MILLENNIUM_API.callable(__call_server_method__, route);
630
495
  }
631
- const ConstructFunctions = (parts) => {
632
- return parts.join('\n');
633
- };
634
- function generate(code) {
635
- /** Wrap it in a proxy */
496
+ function wrapEntryPoint(code) {
636
497
  return `let PluginEntryPointMain = function() { ${code} return millennium_main; };`;
637
498
  }
638
- function InsertMillennium(type, props) {
639
- const generateBundle = (_, bundle) => {
640
- for (const fileName in bundle) {
641
- if (bundle[fileName].type != 'chunk') {
642
- continue;
643
- }
644
- Logger.Info('millenniumAPI', 'Bundling into ' + ComponentType[type] + ' module... ' + chalk.green.bold('okay'));
645
- let code = ConstructFunctions([
646
- `const MILLENNIUM_IS_CLIENT_MODULE = ${type === ComponentType.Plugin ? 'true' : 'false'};`,
647
- `const pluginName = "${props.strPluginInternalName}";`,
648
- InitializePlugins.toString(),
649
- InitializePlugins.name + '()',
650
- WrappedCallServerMethod,
651
- __wrapped_callable__.toString(),
652
- generate(bundle[fileName].code),
653
- ExecutePluginModule.toString(),
654
- ExecutePluginModule.name + '()',
655
- ]);
656
- if (props.bTersePlugin) {
657
- code = minify_sync(code).code ?? code;
499
+ function insertMillennium(target, props) {
500
+ return {
501
+ name: '',
502
+ generateBundle(_, bundle) {
503
+ for (const fileName in bundle) {
504
+ const chunk = bundle[fileName];
505
+ if (chunk.type !== 'chunk')
506
+ continue;
507
+ let code = `\
508
+ const MILLENNIUM_IS_CLIENT_MODULE = ${target === BuildTarget.Plugin};
509
+ const pluginName = "${props.pluginName}";
510
+ ${InitializePlugins.toString()}
511
+ ${InitializePlugins.name}()
512
+ ${kCallServerMethod}
513
+ ${__wrapped_callable__.toString()}
514
+ ${wrapEntryPoint(chunk.code)}
515
+ ${ExecutePluginModule.toString()}
516
+ ${ExecutePluginModule.name}()`;
517
+ if (props.minify) {
518
+ code = minify_sync(code).code ?? code;
519
+ }
520
+ chunk.code = code;
658
521
  }
659
- bundle[fileName].code = code;
660
- }
522
+ },
661
523
  };
662
- return { name: String(), generateBundle };
663
524
  }
664
- async function GetCustomUserPlugins() {
665
- const ttcConfigPath = new URL(`file://${process.cwd().replace(/\\/g, '/')}/ttc.config.mjs`).href;
666
- if (fs.existsSync('./ttc.config.mjs')) {
667
- const { MillenniumCompilerPlugins } = await import(ttcConfigPath);
668
- Logger.Info('millenniumAPI', 'Loading custom plugins from ttc.config.mjs... ' + chalk.green.bold('okay'));
669
- return MillenniumCompilerPlugins;
670
- }
671
- return [];
525
+ function getFrontendDir(pluginJson) {
526
+ return pluginJson?.frontend ?? 'frontend';
672
527
  }
673
- async function MergePluginList(plugins) {
674
- const customPlugins = await GetCustomUserPlugins();
675
- // Filter out custom plugins that have the same name as input plugins
676
- const filteredCustomPlugins = customPlugins.filter((customPlugin) => !plugins.some((plugin) => plugin.name === customPlugin.name));
677
- // Merge input plugins with the filtered custom plugins
678
- return [...plugins, ...filteredCustomPlugins];
528
+ function resolveEntryFile(frontendDir) {
529
+ return frontendDir === '.' || frontendDir === './' || frontendDir === '' ? './index.tsx' : `./${frontendDir}/index.tsx`;
679
530
  }
680
- async function GetPluginComponents(pluginJson, props) {
681
- let tsConfigPath = '';
682
- const frontendDir = GetFrontEndDirectory(pluginJson);
683
- if (frontendDir === '.' || frontendDir === './') {
684
- tsConfigPath = './tsconfig.json';
685
- }
686
- else {
687
- tsConfigPath = `./${frontendDir}/tsconfig.json`;
688
- }
689
- if (!fs.existsSync(tsConfigPath)) {
690
- tsConfigPath = './tsconfig.json';
691
- }
692
- Logger.Info('millenniumAPI', 'Loading tsconfig from ' + chalk.cyan.bold(tsConfigPath) + '... ' + chalk.green.bold('okay'));
693
- let pluginList = [
694
- typescript({
695
- tsconfig: tsConfigPath,
696
- compilerOptions: {
697
- outDir: undefined,
698
- },
699
- }),
700
- url({
701
- include: ['**/*.gif', '**/*.webm', '**/*.svg'], // Add all non-JS assets you use
702
- limit: 0, // Set to 0 to always copy the file instead of inlining as base64
703
- fileName: '[hash][extname]', // Optional: custom output naming
704
- }),
705
- InsertMillennium(ComponentType.Plugin, props),
706
- nodeResolve({
707
- browser: true,
708
- }),
709
- commonjs(),
710
- nodePolyfills(),
711
- scss({
712
- output: false,
713
- outputStyle: 'compressed',
714
- sourceMap: false,
715
- watch: 'src/styles',
716
- sass: sass,
717
- }),
718
- json(),
719
- constSysfsExpr(),
720
- replace({
721
- delimiters: ['', ''],
722
- preventAssignment: true,
723
- 'process.env.NODE_ENV': JSON.stringify('production'),
724
- 'Millennium.callServerMethod': `__call_server_method__`,
725
- 'client.callable': `__wrapped_callable__`,
726
- 'client.pluginSelf': 'window.PLUGIN_LIST[pluginName]',
727
- 'client.Millennium.exposeObj(': 'client.Millennium.exposeObj(exports, ',
728
- 'client.BindPluginSettings()': 'client.BindPluginSettings(pluginName)',
729
- }),
730
- ];
731
- if (envVars.length > 0) {
732
- pluginList.push(injectProcessEnv(envVars));
733
- }
734
- if (props.bTersePlugin) {
735
- pluginList.push(terser());
736
- }
737
- return pluginList;
531
+ function resolveTsConfig(frontendDir) {
532
+ if (frontendDir === '.' || frontendDir === './')
533
+ return './tsconfig.json';
534
+ const candidate = `./${frontendDir}/tsconfig.json`;
535
+ return fs.existsSync(candidate) ? candidate : './tsconfig.json';
738
536
  }
739
- async function GetWebkitPluginComponents(props) {
740
- let pluginList = [
741
- InsertMillennium(ComponentType.Webkit, props),
742
- typescript({
743
- tsconfig: './webkit/tsconfig.json',
744
- }),
745
- url({
746
- include: ['**/*.mp4', '**/*.webm', '**/*.ogg'],
747
- limit: 0, // do NOT inline
748
- fileName: '[name][extname]',
749
- destDir: 'dist/assets', // or adjust as needed
750
- }),
751
- resolve(),
752
- commonjs(),
753
- json(),
754
- constSysfsExpr(),
755
- replace({
756
- delimiters: ['', ''],
757
- preventAssignment: true,
758
- 'Millennium.callServerMethod': `__call_server_method__`,
759
- 'webkit.callable': `__wrapped_callable__`,
760
- 'webkit.Millennium.exposeObj(': 'webkit.Millennium.exposeObj(exports, ',
761
- 'client.BindPluginSettings()': 'client.BindPluginSettings(pluginName)',
762
- }),
763
- babel({
764
- presets: ['@babel/preset-env', '@babel/preset-react'],
765
- babelHelpers: 'bundled',
766
- }),
767
- ];
768
- if (envVars.length > 0) {
769
- pluginList.push(injectProcessEnv(envVars));
770
- }
771
- pluginList = await MergePluginList(pluginList);
772
- props.bTersePlugin && pluginList.push(terser());
773
- return pluginList;
537
+ async function withUserPlugins(plugins) {
538
+ if (!fs.existsSync('./ttc.config.mjs'))
539
+ return plugins;
540
+ const configUrl = pathToFileURL(path.resolve('./ttc.config.mjs')).href;
541
+ const { MillenniumCompilerPlugins } = await import(configUrl);
542
+ const deduped = MillenniumCompilerPlugins.filter((custom) => !plugins.some((p) => p?.name === custom?.name));
543
+ return [...plugins, ...deduped];
774
544
  }
775
- const GetFrontEndDirectory = (pluginJson) => {
776
- try {
777
- return pluginJson?.frontend ?? 'frontend';
545
+ function logLocation(log) {
546
+ const file = log.loc?.file ?? log.id;
547
+ if (!file || !log.loc)
548
+ return undefined;
549
+ return `${path.relative(process.cwd(), file)}:${log.loc.line}:${log.loc.column}`;
550
+ }
551
+ function stripPluginPrefix(message) {
552
+ message = message.replace(/^\[plugin [^\]]+\]\s*/, '');
553
+ message = message.replace(/^\S[^(]*\(\d+:\d+\):\s*/, '');
554
+ message = message.replace(/^@?[\w-]+\/[\w-]+\s+/, '');
555
+ return message;
556
+ }
557
+ class MillenniumBuild {
558
+ isExternal(id) {
559
+ const hint = this.forbidden.get(id);
560
+ if (hint) {
561
+ Logger.error(`${id} cannot be used here; ${hint}`);
562
+ process.exit(1);
563
+ }
564
+ return this.externals.has(id);
778
565
  }
779
- catch (error) {
780
- return 'frontend';
566
+ async build(input, sysfsPlugin, isMillennium) {
567
+ let hasErrors = false;
568
+ const config = {
569
+ input,
570
+ plugins: await this.plugins(sysfsPlugin),
571
+ onwarn: (warning) => {
572
+ const msg = stripPluginPrefix(warning.message);
573
+ const loc = logLocation(warning);
574
+ if (warning.plugin === 'typescript') {
575
+ Logger.error(msg, loc);
576
+ hasErrors = true;
577
+ }
578
+ else {
579
+ Logger.warn(msg, loc);
580
+ }
581
+ },
582
+ context: 'window',
583
+ external: (id) => this.isExternal(id),
584
+ output: this.output(isMillennium),
585
+ };
586
+ await (await rollup(config)).write(config.output);
587
+ if (hasErrors)
588
+ process.exit(1);
781
589
  }
782
- };
783
- const TranspilerPluginComponent = async (bIsMillennium, pluginJson, props) => {
784
- const frontendDir = GetFrontEndDirectory(pluginJson);
785
- console.log(chalk.greenBright.bold('config'), 'Frontend directory set to:', chalk.cyan.bold(frontendDir));
786
- const frontendPlugins = await GetPluginComponents(pluginJson, props);
787
- // Fix entry file path construction
788
- let entryFile = '';
789
- if (frontendDir === '.' || frontendDir === './' || frontendDir === '') {
790
- entryFile = './index.tsx';
590
+ }
591
+ class FrontendBuild extends MillenniumBuild {
592
+ constructor(frontendDir, props) {
593
+ super();
594
+ this.frontendDir = frontendDir;
595
+ this.props = props;
596
+ this.externals = new Set(['@steambrew/client', 'react', 'react-dom', 'react-dom/client', 'react/jsx-runtime']);
597
+ this.forbidden = new Map([['@steambrew/webkit', 'use @steambrew/client in the frontend module']]);
791
598
  }
792
- else {
793
- entryFile = `./${frontendDir}/index.tsx`;
599
+ plugins(sysfsPlugin) {
600
+ const tsPlugin = typescript({ tsconfig: resolveTsConfig(this.frontendDir), compilerOptions: { noCheck: !this.props.minify, outDir: undefined } });
601
+ return [
602
+ tsPlugin,
603
+ url({ include: ['**/*.gif', '**/*.webm', '**/*.svg'], limit: 0, fileName: '[hash][extname]' }),
604
+ insertMillennium(BuildTarget.Plugin, this.props),
605
+ nodeResolve({ browser: true }),
606
+ commonjs(),
607
+ nodePolyfills(),
608
+ scss({ output: false, outputStyle: 'compressed', sourceMap: false, watch: 'src/styles', sass }),
609
+ json(),
610
+ sysfsPlugin,
611
+ replace({
612
+ delimiters: ['', ''],
613
+ preventAssignment: true,
614
+ 'process.env.NODE_ENV': JSON.stringify('production'),
615
+ 'Millennium.callServerMethod': '__call_server_method__',
616
+ 'client.callable': '__wrapped_callable__',
617
+ 'client.pluginSelf': 'window.PLUGIN_LIST[pluginName]',
618
+ 'client.Millennium.exposeObj(': 'client.Millennium.exposeObj(exports, ',
619
+ 'client.BindPluginSettings()': 'client.BindPluginSettings(pluginName)',
620
+ }),
621
+ ...(Object.keys(env).length > 0 ? [injectProcessEnv(env)] : []),
622
+ ...(this.props.minify ? [terser()] : []),
623
+ ];
794
624
  }
795
- console.log(chalk.greenBright.bold('config'), 'Entry file set to:', chalk.cyan.bold(entryFile));
796
- const frontendRollupConfig = {
797
- input: entryFile,
798
- plugins: frontendPlugins,
799
- context: 'window',
800
- external: (id) => {
801
- if (id === '@steambrew/webkit') {
802
- Logger.Error('The @steambrew/webkit module should not be included in the frontend module, use @steambrew/client instead. Please remove it from the frontend module and try again.');
803
- process.exit(1);
804
- }
805
- return id === '@steambrew/client' || id === 'react' || id === 'react-dom' || id === 'react-dom/client' || id === 'react/jsx-runtime';
806
- },
807
- output: {
625
+ output(isMillennium) {
626
+ return {
808
627
  name: 'millennium_main',
809
- file: bIsMillennium ? '../../build/frontend.bin' : '.millennium/Dist/index.js',
628
+ file: isMillennium ? '../../build/frontend.bin' : '.millennium/Dist/index.js',
810
629
  globals: {
811
630
  react: 'window.SP_REACT',
812
631
  'react-dom': 'window.SP_REACTDOM',
@@ -816,38 +635,68 @@ const TranspilerPluginComponent = async (bIsMillennium, pluginJson, props) => {
816
635
  },
817
636
  exports: 'named',
818
637
  format: 'iife',
819
- },
820
- };
638
+ };
639
+ }
640
+ }
641
+ class WebkitBuild extends MillenniumBuild {
642
+ constructor(props) {
643
+ super();
644
+ this.props = props;
645
+ this.externals = new Set(['@steambrew/webkit']);
646
+ this.forbidden = new Map([['@steambrew/client', 'use @steambrew/webkit in the webkit module']]);
647
+ }
648
+ async plugins(sysfsPlugin) {
649
+ const tsPlugin = typescript({ tsconfig: './webkit/tsconfig.json', compilerOptions: { noCheck: !this.props.minify } });
650
+ const base = [
651
+ insertMillennium(BuildTarget.Webkit, this.props),
652
+ tsPlugin,
653
+ url({ include: ['**/*.mp4', '**/*.webm', '**/*.ogg'], limit: 0, fileName: '[name][extname]', destDir: 'dist/assets' }),
654
+ resolve(),
655
+ commonjs(),
656
+ json(),
657
+ sysfsPlugin,
658
+ replace({
659
+ delimiters: ['', ''],
660
+ preventAssignment: true,
661
+ 'Millennium.callServerMethod': '__call_server_method__',
662
+ 'webkit.callable': '__wrapped_callable__',
663
+ 'webkit.Millennium.exposeObj(': 'webkit.Millennium.exposeObj(exports, ',
664
+ 'client.BindPluginSettings()': 'client.BindPluginSettings(pluginName)',
665
+ }),
666
+ babel({ presets: ['@babel/preset-env', '@babel/preset-react'], babelHelpers: 'bundled' }),
667
+ ...(Object.keys(env).length > 0 ? [injectProcessEnv(env)] : []),
668
+ ];
669
+ const merged = await withUserPlugins(base);
670
+ return this.props.minify ? [...merged, terser()] : merged;
671
+ }
672
+ output(_isMillennium) {
673
+ return {
674
+ name: 'millennium_main',
675
+ file: '.millennium/Dist/webkit.js',
676
+ exports: 'named',
677
+ format: 'iife',
678
+ globals: { '@steambrew/webkit': 'window.MILLENNIUM_API' },
679
+ };
680
+ }
681
+ }
682
+ const TranspilerPluginComponent = async (isMillennium, pluginJson, props) => {
683
+ const webkitDir = './webkit/index.tsx';
684
+ const frontendDir = getFrontendDir(pluginJson);
685
+ const sysfs = constSysfsExpr();
821
686
  try {
822
- await (await rollup(frontendRollupConfig)).write(frontendRollupConfig.output);
823
- if (fs.existsSync(`./webkit/index.tsx`)) {
824
- const webkitRollupConfig = {
825
- input: `./webkit/index.tsx`,
826
- plugins: await GetWebkitPluginComponents(props),
827
- context: 'window',
828
- external: (id) => {
829
- if (id === '@steambrew/client') {
830
- Logger.Error('The @steambrew/client module should not be included in the webkit module, use @steambrew/webkit instead. Please remove it from the webkit module and try again.');
831
- process.exit(1);
832
- }
833
- return id === '@steambrew/webkit';
834
- },
835
- output: {
836
- name: 'millennium_main',
837
- file: '.millennium/Dist/webkit.js',
838
- exports: 'named',
839
- format: 'iife',
840
- globals: {
841
- '@steambrew/webkit': 'window.MILLENNIUM_API',
842
- },
843
- },
844
- };
845
- await (await rollup(webkitRollupConfig)).write(webkitRollupConfig.output);
687
+ await new FrontendBuild(frontendDir, props).build(resolveEntryFile(frontendDir), sysfs.plugin, isMillennium);
688
+ if (fs.existsSync(webkitDir)) {
689
+ await new WebkitBuild(props).build(webkitDir, sysfs.plugin, isMillennium);
846
690
  }
847
- Logger.Info('build', 'Succeeded passing all tests in', Number((performance.now() - global.PerfStartTime).toFixed(3)), 'ms elapsed.');
691
+ Logger.done({
692
+ elapsedMs: performance.now() - global.PerfStartTime,
693
+ buildType: props.minify ? 'prod' : 'dev',
694
+ sysfsCount: sysfs.getCount() || undefined,
695
+ envCount: Object.keys(env).length || undefined,
696
+ });
848
697
  }
849
698
  catch (exception) {
850
- Logger.Error('error', 'Build failed!', exception);
699
+ Logger.error(stripPluginPrefix(exception?.message ?? String(exception)), logLocation(exception));
851
700
  process.exit(1);
852
701
  }
853
702
  };
@@ -864,18 +713,14 @@ const StartCompilerModule = () => {
864
713
  const parameters = ValidateParameters(process.argv.slice(2));
865
714
  const bIsMillennium = parameters.isMillennium || false;
866
715
  const bTersePlugin = parameters.type == BuildType.ProdBuild;
867
- console.log(chalk.greenBright.bold('config'), 'Building target:', parameters.targetPlugin, 'with type:', BuildType[parameters.type], 'minify:', bTersePlugin, '...');
868
716
  ValidatePlugin(bIsMillennium, parameters.targetPlugin)
869
717
  .then((json) => {
870
718
  const props = {
871
- bTersePlugin: bTersePlugin,
872
- strPluginInternalName: json?.name,
719
+ minify: bTersePlugin,
720
+ pluginName: json?.name,
873
721
  };
874
722
  TranspilerPluginComponent(bIsMillennium, json, props);
875
723
  })
876
- /**
877
- * plugin is invalid, we close the proccess as it has already been handled
878
- */
879
724
  .catch(() => {
880
725
  process.exit();
881
726
  });