@steambrew/ttc 2.8.7 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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';
@@ -11,9 +11,10 @@ import resolve, { nodeResolve } from '@rollup/plugin-node-resolve';
11
11
  import replace from '@rollup/plugin-replace';
12
12
  import terser from '@rollup/plugin-terser';
13
13
  import typescript from '@rollup/plugin-typescript';
14
+ import esbuild from 'rollup-plugin-esbuild';
14
15
  import url from '@rollup/plugin-url';
15
- import { rollup } from 'rollup';
16
16
  import nodePolyfills from 'rollup-plugin-polyfill-node';
17
+ import { rollup } from 'rollup';
17
18
  import { minify_sync } from 'terser';
18
19
  import scss from 'rollup-plugin-scss';
19
20
  import * as sass from 'sass';
@@ -26,57 +27,53 @@ import MagicString from 'magic-string';
26
27
  import _traverse from '@babel/traverse';
27
28
  import { performance as performance$1 } from 'perf_hooks';
28
29
 
30
+ const version = JSON.parse(readFileSync(path.resolve(dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf8')).version;
29
31
  const Logger = {
30
- Info: (name, ...LogMessage) => {
31
- console.log(chalk.magenta.bold(name), ...LogMessage);
32
+ warn(message, loc) {
33
+ if (loc) {
34
+ console.warn(`${chalk.dim(loc + ':')} ${message}`);
35
+ }
36
+ else {
37
+ console.warn(`${chalk.yellow('warning:')} ${message}`);
38
+ }
32
39
  },
33
- Warn: (...LogMessage) => {
34
- console.log(chalk.yellow.bold('**'), ...LogMessage);
40
+ error(message, loc) {
41
+ if (loc) {
42
+ console.error(`${chalk.dim(loc + ':')} ${message}`);
43
+ }
44
+ else {
45
+ console.error(`${chalk.red('error:')} ${message}`);
46
+ }
35
47
  },
36
- Error: (...LogMessage) => {
37
- console.error(chalk.red.bold('!!'), ...LogMessage);
48
+ update(current, latest, installCmd) {
49
+ console.log(`\n${chalk.yellow('update available')} ${chalk.dim(current)} → ${chalk.green(latest)}`);
50
+ console.log(`${chalk.dim('run:')} ${installCmd}\n`);
38
51
  },
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
- }
52
+ done({ elapsedMs, buildType, sysfsCount, envCount }) {
53
+ const elapsed = `${(elapsedMs / 1000).toFixed(2)}s`;
54
+ const meta = [`ttc v${version}`];
55
+ if (buildType === 'dev')
56
+ meta.push('no type checking');
57
+ if (sysfsCount)
58
+ meta.push(`${sysfsCount} constSysfsExpr`);
59
+ if (envCount)
60
+ meta.push(`${envCount} env var${envCount > 1 ? 's' : ''}`);
61
+ console.log(`${chalk.green('Finished')} ${buildType} in ${elapsed} ` + chalk.dim('(' + meta.join(', ') + ')'));
61
62
  },
62
63
  };
63
64
 
64
- /***
65
- * @brief print the parameter list to the stdout
66
- */
67
65
  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');
66
+ console.log([
67
+ '',
68
+ 'usage: millennium-ttc --build <dev|prod> [options]',
69
+ '',
70
+ 'options:',
71
+ ' --build <dev|prod> build type (prod enables minification)',
72
+ ' --target <path> plugin directory (default: current directory)',
73
+ ' --no-update skip update check',
74
+ ' --help show this message',
75
+ '',
76
+ ].join('\n'));
80
77
  };
81
78
  var BuildType;
82
79
  (function (BuildType) {
@@ -89,9 +86,8 @@ const ValidateParameters = (args) => {
89
86
  PrintParamHelp();
90
87
  process.exit();
91
88
  }
92
- // startup args are invalid
93
89
  if (!args.includes('--build')) {
94
- Logger.Error('Received invalid arguments...');
90
+ Logger.error('missing required argument: --build');
95
91
  PrintParamHelp();
96
92
  process.exit();
97
93
  }
@@ -106,14 +102,14 @@ const ValidateParameters = (args) => {
106
102
  typeProp = BuildType.ProdBuild;
107
103
  break;
108
104
  default: {
109
- Logger.Error('--build parameter must be preceded by build type [dev, prod]');
105
+ Logger.error('--build must be one of: dev, prod');
110
106
  process.exit();
111
107
  }
112
108
  }
113
109
  }
114
110
  if (args[i] == '--target') {
115
111
  if (args[i + 1] === undefined) {
116
- Logger.Error('--target parameter must be preceded by system path');
112
+ Logger.error('--target requires a path argument');
117
113
  process.exit();
118
114
  }
119
115
  targetProp = args[i + 1];
@@ -129,40 +125,61 @@ const ValidateParameters = (args) => {
129
125
  };
130
126
  };
131
127
 
128
+ async function fileExists(filePath) {
129
+ return access(filePath).then(() => true).catch(() => false);
130
+ }
131
+ async function detectPackageManager() {
132
+ const userAgent = process.env.npm_config_user_agent ?? '';
133
+ if (userAgent.startsWith('bun'))
134
+ return 'bun';
135
+ if (userAgent.startsWith('pnpm'))
136
+ return 'pnpm';
137
+ if (userAgent.startsWith('yarn'))
138
+ return 'yarn';
139
+ const cwd = process.cwd();
140
+ if (await fileExists(path.join(cwd, 'bun.lock')) || await fileExists(path.join(cwd, 'bun.lockb')))
141
+ return 'bun';
142
+ if (await fileExists(path.join(cwd, 'pnpm-lock.yaml')))
143
+ return 'pnpm';
144
+ if (await fileExists(path.join(cwd, 'yarn.lock')))
145
+ return 'yarn';
146
+ return 'npm';
147
+ }
148
+ function installCommand(pm, pkg) {
149
+ switch (pm) {
150
+ case 'bun': return `bun add -d ${pkg}`;
151
+ case 'pnpm': return `pnpm add -D ${pkg}`;
152
+ case 'yarn': return `yarn add -D ${pkg}`;
153
+ default: return `npm i -D ${pkg}`;
154
+ }
155
+ }
132
156
  const CheckForUpdates = async () => {
133
- return new Promise(async (resolve) => {
157
+ try {
134
158
  const packageJsonPath = path.resolve(dirname(fileURLToPath(import.meta.url)), '../package.json');
135
159
  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
- });
160
+ const response = await fetch('https://registry.npmjs.org/@steambrew/ttc');
161
+ const json = await response.json();
162
+ const latest = json?.['dist-tags']?.latest;
163
+ if (latest && latest !== packageJson.version) {
164
+ const pm = await detectPackageManager();
165
+ Logger.update(packageJson.version, latest, installCommand(pm, `@steambrew/ttc@${latest}`));
166
+ return true;
167
+ }
168
+ }
169
+ catch {
170
+ // network or parse failure — silently skip
171
+ }
172
+ return false;
155
173
  };
156
174
 
157
175
  const ValidatePlugin = (bIsMillennium, target) => {
158
176
  return new Promise((resolve, reject) => {
159
177
  if (!existsSync(target)) {
160
- console.error(chalk.red.bold(`\n[-] --target [${target}] `) + chalk.red('is not a valid system path'));
178
+ Logger.error(`target path does not exist: ${target}`);
161
179
  reject();
162
180
  return;
163
181
  }
164
182
  if (bIsMillennium) {
165
- console.log(chalk.green.bold('\n[+] Using Millennium internal build configuration'));
166
183
  resolve({
167
184
  name: 'core',
168
185
  common_name: 'Millennium',
@@ -174,27 +191,28 @@ const ValidatePlugin = (bIsMillennium, target) => {
174
191
  }
175
192
  const pluginModule = path.join(target, 'plugin.json');
176
193
  if (!existsSync(pluginModule)) {
177
- console.error(chalk.red.bold(`\n[-] --target [${target}] `) + chalk.red('is not a valid plugin (missing plugin.json)'));
194
+ Logger.error(`plugin.json not found: ${pluginModule}`);
178
195
  reject();
179
196
  return;
180
197
  }
181
198
  readFile$1(pluginModule, 'utf8', (err, data) => {
182
199
  if (err) {
183
- console.error(chalk.red.bold(`\n[-] couldn't read plugin.json from [${pluginModule}]`));
200
+ Logger.error(`could not read plugin.json: ${pluginModule}`);
184
201
  reject();
185
202
  return;
186
203
  }
187
204
  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}]`));
205
+ const parsed = JSON.parse(data);
206
+ if (!('name' in parsed)) {
207
+ Logger.error('plugin.json is missing required "name" field');
190
208
  reject();
191
209
  }
192
210
  else {
193
- resolve(JSON.parse(data));
211
+ resolve(parsed);
194
212
  }
195
213
  }
196
214
  catch (parseError) {
197
- console.error(chalk.red.bold(`\n[-] couldn't parse JSON in plugin.json from [${pluginModule}]`));
215
+ Logger.error('plugin.json contains invalid JSON:', pluginModule);
198
216
  reject();
199
217
  }
200
218
  });
@@ -205,178 +223,42 @@ const ValidatePlugin = (bIsMillennium, target) => {
205
223
  * @description Append the active plugin to the global plugin
206
224
  * list and notify that the frontend Loaded.
207
225
  */
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
- }
226
+ async function ExecutePluginModule() {
227
+ let PluginModule = PluginEntryPointMain();
228
+ /** Assign the plugin on plugin list. */
229
+ Object.assign(window.PLUGIN_LIST[pluginName], {
230
+ ...PluginModule,
231
+ __millennium_internal_plugin_name_do_not_use_or_change__: pluginName,
254
232
  });
233
+ /** Run the rolled up plugins default exported function */
234
+ let pluginProps = await PluginModule.default();
235
+ function isValidSidebarNavComponent(obj) {
236
+ return obj && obj.title !== undefined && obj.icon !== undefined && obj.content !== undefined;
237
+ }
238
+ if (pluginProps && isValidSidebarNavComponent(pluginProps)) {
239
+ window.MILLENNIUM_SIDEBAR_NAVIGATION_PANELS[pluginName] = pluginProps;
240
+ }
241
+ /** If the current module is a client module, post message id=1 which calls the front_end_loaded method on the backend. */
242
+ if (MILLENNIUM_IS_CLIENT_MODULE) {
243
+ MILLENNIUM_BACKEND_IPC.postMessage(1, { pluginName: pluginName });
244
+ }
255
245
  }
256
246
  /**
257
247
  * @description Initialize the plugins settings store and the plugin list.
258
248
  * This function is called once per plugin and is used to store the plugin settings and the plugin list.
259
249
  */
260
250
  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
- */
251
+ var _a;
266
252
  (_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
253
  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
254
  }
371
255
 
372
256
  const traverse = _traverse.default;
373
- const Log = (...message) => {
374
- console.log(chalk.blueBright.bold('constSysfsExpr'), ...message);
375
- };
376
257
  function constSysfsExpr(options = {}) {
377
258
  const filter = createFilter(options.include, options.exclude);
378
259
  const pluginName = 'millennium-const-sysfs-expr';
379
- return {
260
+ let count = 0;
261
+ const plugin = {
380
262
  name: pluginName,
381
263
  transform(code, id) {
382
264
  if (!filter(id))
@@ -512,7 +394,6 @@ function constSysfsExpr(options = {}) {
512
394
  return;
513
395
  }
514
396
  try {
515
- const currentLocString = node.loc?.start ? ` at ${id}:${node.loc.start.line}:${node.loc.start.column}` : ` in ${id}`;
516
397
  const searchBasePath = callOptions.basePath
517
398
  ? path.isAbsolute(callOptions.basePath)
518
399
  ? callOptions.basePath
@@ -521,13 +402,11 @@ function constSysfsExpr(options = {}) {
521
402
  ? path.dirname(pathOrPattern)
522
403
  : path.resolve(path.dirname(id), path.dirname(pathOrPattern));
523
404
  let embeddedContent;
524
- let embeddedCount = 0;
525
405
  const isPotentialPattern = /[?*+!@()[\]{}]/.test(pathOrPattern);
526
406
  if (!isPotentialPattern &&
527
407
  fs.existsSync(path.resolve(searchBasePath, pathOrPattern)) &&
528
408
  fs.statSync(path.resolve(searchBasePath, pathOrPattern)).isFile()) {
529
409
  const singleFilePath = path.resolve(searchBasePath, pathOrPattern);
530
- Log(`Mode: Single file (first argument "${pathOrPattern}" resolved to "${singleFilePath}" relative to "${searchBasePath}")`);
531
410
  try {
532
411
  const rawContent = fs.readFileSync(singleFilePath, callOptions.encoding);
533
412
  const contentString = rawContent.toString();
@@ -537,8 +416,6 @@ function constSysfsExpr(options = {}) {
537
416
  fileName: path.relative(searchBasePath, singleFilePath),
538
417
  };
539
418
  embeddedContent = JSON.stringify(fileInfo);
540
- embeddedCount = 1;
541
- Log(`Embedded 1 specific file for call${currentLocString}`);
542
419
  }
543
420
  catch (fileError) {
544
421
  let message = String(fileError instanceof Error ? fileError.message : fileError ?? 'Unknown file read error');
@@ -547,8 +424,6 @@ function constSysfsExpr(options = {}) {
547
424
  }
548
425
  }
549
426
  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
427
  const matchingFiles = glob.sync(pathOrPattern, {
553
428
  cwd: searchBasePath,
554
429
  nodir: true,
@@ -571,15 +446,13 @@ function constSysfsExpr(options = {}) {
571
446
  }
572
447
  }
573
448
  embeddedContent = JSON.stringify(fileInfoArray);
574
- embeddedCount = fileInfoArray.length;
575
- Log(`Embedded ${embeddedCount} file(s) matching pattern for call${currentLocString}`);
576
449
  }
577
450
  // Replace the call expression with the generated content string
578
451
  magicString.overwrite(node.start, node.end, embeddedContent);
579
452
  hasReplaced = true;
453
+ count++;
580
454
  }
581
455
  catch (error) {
582
- console.error(`Failed to process files for constSysfsExpr call in ${id}:`, error);
583
456
  const message = String(error instanceof Error ? error.message : error ?? 'Unknown error during file processing');
584
457
  this.error(`Could not process files for constSysfsExpr: ${message}`, node.loc?.start.index);
585
458
  return;
@@ -589,7 +462,6 @@ function constSysfsExpr(options = {}) {
589
462
  });
590
463
  }
591
464
  catch (error) {
592
- console.error(`Error parsing or traversing ${id}:`, error);
593
465
  const message = String(error instanceof Error ? error.message : error ?? 'Unknown parsing error');
594
466
  this.error(`Failed to parse ${id}: ${message}`);
595
467
  return null;
@@ -606,207 +478,157 @@ function constSysfsExpr(options = {}) {
606
478
  return result;
607
479
  },
608
480
  };
481
+ return { plugin, getCount: () => count };
609
482
  }
610
483
 
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)';
484
+ const env = dotenv.config().parsed ?? {};
485
+ var BuildTarget;
486
+ (function (BuildTarget) {
487
+ BuildTarget[BuildTarget["Plugin"] = 0] = "Plugin";
488
+ BuildTarget[BuildTarget["Webkit"] = 1] = "Webkit";
489
+ })(BuildTarget || (BuildTarget = {}));
490
+ const kCallServerMethod = 'const __call_server_method__ = (methodName, kwargs) => Millennium.callServerMethod(pluginName, methodName, kwargs)';
625
491
  function __wrapped_callable__(route) {
626
492
  if (route.startsWith('webkit:')) {
627
493
  return MILLENNIUM_API.callable((methodName, kwargs) => MILLENNIUM_API.__INTERNAL_CALL_WEBKIT_METHOD__(pluginName, methodName, kwargs), route.replace(/^webkit:/, ''));
628
494
  }
629
495
  return MILLENNIUM_API.callable(__call_server_method__, route);
630
496
  }
631
- const ConstructFunctions = (parts) => {
632
- return parts.join('\n');
633
- };
634
- function generate(code) {
635
- /** Wrap it in a proxy */
497
+ function wrapEntryPoint(code) {
636
498
  return `let PluginEntryPointMain = function() { ${code} return millennium_main; };`;
637
499
  }
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;
500
+ function insertMillennium(target, props) {
501
+ return {
502
+ name: '',
503
+ generateBundle(_, bundle) {
504
+ for (const fileName in bundle) {
505
+ const chunk = bundle[fileName];
506
+ if (chunk.type !== 'chunk')
507
+ continue;
508
+ let code = `\
509
+ const MILLENNIUM_IS_CLIENT_MODULE = ${target === BuildTarget.Plugin};
510
+ const pluginName = "${props.pluginName}";
511
+ ${InitializePlugins.toString()}
512
+ ${InitializePlugins.name}()
513
+ ${kCallServerMethod}
514
+ ${__wrapped_callable__.toString()}
515
+ ${wrapEntryPoint(chunk.code)}
516
+ ${ExecutePluginModule.toString()}
517
+ ${ExecutePluginModule.name}()`;
518
+ if (props.minify) {
519
+ code = minify_sync(code).code ?? code;
520
+ }
521
+ chunk.code = code;
658
522
  }
659
- bundle[fileName].code = code;
660
- }
523
+ },
661
524
  };
662
- return { name: String(), generateBundle };
663
525
  }
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 [];
526
+ function getFrontendDir(pluginJson) {
527
+ return pluginJson?.frontend ?? 'frontend';
672
528
  }
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];
529
+ function resolveEntryFile(frontendDir) {
530
+ return frontendDir === '.' || frontendDir === './' || frontendDir === '' ? './index.tsx' : `./${frontendDir}/index.tsx`;
679
531
  }
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;
532
+ function resolveTsConfig(frontendDir) {
533
+ if (frontendDir === '.' || frontendDir === './')
534
+ return './tsconfig.json';
535
+ const candidate = `./${frontendDir}/tsconfig.json`;
536
+ return fs.existsSync(candidate) ? candidate : './tsconfig.json';
738
537
  }
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;
538
+ async function withUserPlugins(plugins) {
539
+ if (!fs.existsSync('./ttc.config.mjs'))
540
+ return plugins;
541
+ const configUrl = pathToFileURL(path.resolve('./ttc.config.mjs')).href;
542
+ const { MillenniumCompilerPlugins } = await import(configUrl);
543
+ const deduped = MillenniumCompilerPlugins.filter((custom) => !plugins.some((p) => p?.name === custom?.name));
544
+ return [...plugins, ...deduped];
774
545
  }
775
- const GetFrontEndDirectory = (pluginJson) => {
776
- try {
777
- return pluginJson?.frontend ?? 'frontend';
546
+ function logLocation(log) {
547
+ const file = log.loc?.file ?? log.id;
548
+ if (!file || !log.loc)
549
+ return undefined;
550
+ return `${path.relative(process.cwd(), file)}:${log.loc.line}:${log.loc.column}`;
551
+ }
552
+ function stripPluginPrefix(message) {
553
+ message = message.replace(/^\[plugin [^\]]+\]\s*/, '');
554
+ message = message.replace(/^\S[^(]*\(\d+:\d+\):\s*/, '');
555
+ message = message.replace(/^@?[\w-]+\/[\w-]+\s+/, '');
556
+ return message;
557
+ }
558
+ class MillenniumBuild {
559
+ isExternal(id) {
560
+ const hint = this.forbidden.get(id);
561
+ if (hint) {
562
+ Logger.error(`${id} cannot be used here; ${hint}`);
563
+ process.exit(1);
564
+ }
565
+ return this.externals.has(id);
778
566
  }
779
- catch (error) {
780
- return 'frontend';
567
+ async build(input, sysfsPlugin, isMillennium) {
568
+ let hasErrors = false;
569
+ const config = {
570
+ input,
571
+ plugins: await this.plugins(sysfsPlugin),
572
+ onwarn: (warning) => {
573
+ const msg = stripPluginPrefix(warning.message);
574
+ const loc = logLocation(warning);
575
+ if (warning.plugin === 'typescript') {
576
+ Logger.error(msg, loc);
577
+ hasErrors = true;
578
+ }
579
+ else {
580
+ Logger.warn(msg, loc);
581
+ }
582
+ },
583
+ context: 'window',
584
+ external: (id) => this.isExternal(id),
585
+ output: this.output(isMillennium),
586
+ };
587
+ await (await rollup(config)).write(config.output);
588
+ if (hasErrors)
589
+ process.exit(1);
781
590
  }
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';
591
+ }
592
+ class FrontendBuild extends MillenniumBuild {
593
+ constructor(frontendDir, props) {
594
+ super();
595
+ this.frontendDir = frontendDir;
596
+ this.props = props;
597
+ this.externals = new Set(['@steambrew/client', 'react', 'react-dom', 'react-dom/client', 'react/jsx-runtime']);
598
+ this.forbidden = new Map([['@steambrew/webkit', 'use @steambrew/client in the frontend module']]);
791
599
  }
792
- else {
793
- entryFile = `./${frontendDir}/index.tsx`;
600
+ plugins(sysfsPlugin) {
601
+ const tsPlugin = this.props.minify
602
+ ? typescript({ tsconfig: resolveTsConfig(this.frontendDir), compilerOptions: { outDir: undefined } })
603
+ : esbuild({ tsconfig: resolveTsConfig(this.frontendDir) });
604
+ return [
605
+ tsPlugin,
606
+ url({ include: ['**/*.gif', '**/*.webm', '**/*.svg'], limit: 0, fileName: '[hash][extname]' }),
607
+ insertMillennium(BuildTarget.Plugin, this.props),
608
+ nodeResolve({ browser: true }),
609
+ commonjs(),
610
+ nodePolyfills(),
611
+ scss({ output: false, outputStyle: 'compressed', sourceMap: false, watch: 'src/styles', sass }),
612
+ json(),
613
+ sysfsPlugin,
614
+ replace({
615
+ delimiters: ['', ''],
616
+ preventAssignment: true,
617
+ 'process.env.NODE_ENV': JSON.stringify('production'),
618
+ 'Millennium.callServerMethod': '__call_server_method__',
619
+ 'client.callable': '__wrapped_callable__',
620
+ 'client.pluginSelf': 'window.PLUGIN_LIST[pluginName]',
621
+ 'client.Millennium.exposeObj(': 'client.Millennium.exposeObj(exports, ',
622
+ 'client.BindPluginSettings()': 'client.BindPluginSettings(pluginName)',
623
+ }),
624
+ ...(Object.keys(env).length > 0 ? [injectProcessEnv(env)] : []),
625
+ ...(this.props.minify ? [terser()] : []),
626
+ ];
794
627
  }
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: {
628
+ output(isMillennium) {
629
+ return {
808
630
  name: 'millennium_main',
809
- file: bIsMillennium ? '../../build/frontend.bin' : '.millennium/Dist/index.js',
631
+ file: isMillennium ? '../../build/frontend.bin' : '.millennium/Dist/index.js',
810
632
  globals: {
811
633
  react: 'window.SP_REACT',
812
634
  'react-dom': 'window.SP_REACTDOM',
@@ -816,38 +638,70 @@ const TranspilerPluginComponent = async (bIsMillennium, pluginJson, props) => {
816
638
  },
817
639
  exports: 'named',
818
640
  format: 'iife',
819
- },
820
- };
641
+ };
642
+ }
643
+ }
644
+ class WebkitBuild extends MillenniumBuild {
645
+ constructor(props) {
646
+ super();
647
+ this.props = props;
648
+ this.externals = new Set(['@steambrew/webkit']);
649
+ this.forbidden = new Map([['@steambrew/client', 'use @steambrew/webkit in the webkit module']]);
650
+ }
651
+ async plugins(sysfsPlugin) {
652
+ const tsPlugin = this.props.minify
653
+ ? typescript({ tsconfig: './webkit/tsconfig.json' })
654
+ : esbuild({ tsconfig: './webkit/tsconfig.json' });
655
+ const base = [
656
+ insertMillennium(BuildTarget.Webkit, this.props),
657
+ tsPlugin,
658
+ url({ include: ['**/*.mp4', '**/*.webm', '**/*.ogg'], limit: 0, fileName: '[name][extname]', destDir: 'dist/assets' }),
659
+ resolve(),
660
+ commonjs(),
661
+ json(),
662
+ sysfsPlugin,
663
+ replace({
664
+ delimiters: ['', ''],
665
+ preventAssignment: true,
666
+ 'Millennium.callServerMethod': '__call_server_method__',
667
+ 'webkit.callable': '__wrapped_callable__',
668
+ 'webkit.Millennium.exposeObj(': 'webkit.Millennium.exposeObj(exports, ',
669
+ 'client.BindPluginSettings()': 'client.BindPluginSettings(pluginName)',
670
+ }),
671
+ babel({ presets: ['@babel/preset-env', '@babel/preset-react'], babelHelpers: 'bundled' }),
672
+ ...(Object.keys(env).length > 0 ? [injectProcessEnv(env)] : []),
673
+ ];
674
+ const merged = await withUserPlugins(base);
675
+ return this.props.minify ? [...merged, terser()] : merged;
676
+ }
677
+ output(_isMillennium) {
678
+ return {
679
+ name: 'millennium_main',
680
+ file: '.millennium/Dist/webkit.js',
681
+ exports: 'named',
682
+ format: 'iife',
683
+ globals: { '@steambrew/webkit': 'window.MILLENNIUM_API' },
684
+ };
685
+ }
686
+ }
687
+ const TranspilerPluginComponent = async (isMillennium, pluginJson, props) => {
688
+ const webkitDir = './webkit/index.tsx';
689
+ const frontendDir = getFrontendDir(pluginJson);
690
+ const sysfs = constSysfsExpr();
821
691
  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);
692
+ await new FrontendBuild(frontendDir, props).build(resolveEntryFile(frontendDir), sysfs.plugin, isMillennium);
693
+ if (fs.existsSync(webkitDir)) {
694
+ await new WebkitBuild(props).build(webkitDir, sysfs.plugin, isMillennium);
846
695
  }
847
- Logger.Info('build', 'Succeeded passing all tests in', Number((performance.now() - global.PerfStartTime).toFixed(3)), 'ms elapsed.');
696
+ Logger.done({
697
+ elapsedMs: performance.now() - global.PerfStartTime,
698
+ buildType: props.minify ? 'prod' : 'dev',
699
+ sysfsCount: sysfs.getCount() || undefined,
700
+ envCount: Object.keys(env).length || undefined,
701
+ });
848
702
  }
849
703
  catch (exception) {
850
- Logger.Error('error', 'Build failed!', exception);
704
+ Logger.error(stripPluginPrefix(exception?.message ?? String(exception)), logLocation(exception));
851
705
  process.exit(1);
852
706
  }
853
707
  };
@@ -864,18 +718,14 @@ const StartCompilerModule = () => {
864
718
  const parameters = ValidateParameters(process.argv.slice(2));
865
719
  const bIsMillennium = parameters.isMillennium || false;
866
720
  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
721
  ValidatePlugin(bIsMillennium, parameters.targetPlugin)
869
722
  .then((json) => {
870
723
  const props = {
871
- bTersePlugin: bTersePlugin,
872
- strPluginInternalName: json?.name,
724
+ minify: bTersePlugin,
725
+ pluginName: json?.name,
873
726
  };
874
727
  TranspilerPluginComponent(bIsMillennium, json, props);
875
728
  })
876
- /**
877
- * plugin is invalid, we close the proccess as it has already been handled
878
- */
879
729
  .catch(() => {
880
730
  process.exit();
881
731
  });