@qooxdoo/framework 8.0.0-beta.1 → 8.0.0-beta.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/Manifest.json +1 -1
  3. package/lib/compiler/compile-info.json +55 -55
  4. package/lib/compiler/index.js +18497 -22975
  5. package/lib/resource/qx/tool/compiler/cli/templates/class/default.tmpl.js +6 -7
  6. package/lib/resource/qx/tool/compiler/cli/templates/class/singleton.tmpl.js +5 -6
  7. package/lib/resource/qx/tool/compiler/schema/compile-1-0-0.json +6 -2
  8. package/package.json +8 -10
  9. package/source/class/qx/Class.js +3 -3
  10. package/source/class/qx/core/BaseInit.js +14 -13
  11. package/source/class/qx/core/MObjectId.js +16 -0
  12. package/source/class/qx/core/MProperty.js +147 -175
  13. package/source/class/qx/core/check/DynamicTypeCheck.js +4 -0
  14. package/source/class/qx/core/property/Property.js +20 -19
  15. package/source/class/qx/data/SingleValueBinding.js +5 -7
  16. package/source/class/qx/data/binding/AbstractSegment.js +1 -1
  17. package/source/class/qx/data/binding/ArrayIndexSegment.js +17 -10
  18. package/source/class/qx/data/binding/IInputReceiver.js +1 -1
  19. package/source/class/qx/data/binding/PropNameSegment.js +1 -1
  20. package/source/class/qx/dev/unit/TestCase.js +4 -1
  21. package/source/class/qx/event/handler/Focus.js +2 -1
  22. package/source/class/qx/html/Jsx.js +2 -3
  23. package/source/class/qx/html/Node.js +3 -3
  24. package/source/class/qx/io/jsonrpc/Client.js +1 -1
  25. package/source/class/qx/promise/NativeWrapper.js +1 -1
  26. package/source/class/qx/test/core/Property.js +30 -2
  27. package/source/class/qx/test/data/singlevalue/Simple.js +6 -0
  28. package/source/class/qx/test/locale/Date.js +2 -2
  29. package/source/class/qx/theme/classic/Appearance.js +21 -0
  30. package/source/class/qx/theme/modern/Appearance.js +21 -0
  31. package/source/class/qx/theme/simple/Appearance.js +21 -0
  32. package/source/class/qx/theme/tangible/Appearance.js +2 -0
  33. package/source/class/qx/tool/cli/AbstractCliApp.js +18 -2
  34. package/source/class/qx/tool/compiler/ClassFile.js +0 -4
  35. package/source/class/qx/tool/compiler/MetaDatabase.js +47 -0
  36. package/source/class/qx/tool/compiler/cli/commands/Compile.js +139 -8
  37. package/source/class/qx/tool/compiler/cli/commands/Create.js +1 -1
  38. package/source/class/qx/tool/compiler/cli/commands/Serve.js +1 -1
  39. package/source/class/qx/tool/compiler/cli/commands/Typescript.js +26 -39
  40. package/source/class/qx/tool/compiler/cli/commands/add/Script.js +1 -1
  41. package/source/class/qx/tool/compiler/cli/commands/package/Publish.js +3 -2
  42. package/source/class/qx/tool/compiler/cli/commands/package/Update.js +2 -2
  43. package/source/class/qx/tool/compiler/targets/TypeScriptWriter.js +3 -0
  44. package/source/class/qx/tool/compiler/targets/meta/Browserify.js +142 -80
  45. package/source/class/qx/tool/migration/M8_0_0.js +4 -4
  46. package/source/class/qx/ui/toolbar/ToolBar.js +4 -4
  47. package/source/resource/qx/tool/compiler/cli/templates/class/default.tmpl.js +6 -7
  48. package/source/resource/qx/tool/compiler/cli/templates/class/singleton.tmpl.js +5 -6
  49. package/source/resource/qx/tool/compiler/schema/compile-1-0-0.json +6 -2
@@ -33,6 +33,11 @@ qx.Class.define("qx.tool.compiler.cli.commands.Compile", {
33
33
  extend: qx.tool.compiler.cli.Command,
34
34
 
35
35
  statics: {
36
+ /**
37
+ * Creates and configures the CLI command for the compile subcommand
38
+ * @param clazz {Function} the class to instantiate as the command handler
39
+ * @return {Promise<qx.tool.cli.Command>} the configured command
40
+ */
36
41
  async createCliCommand(clazz = this) {
37
42
  let cmd = await qx.tool.compiler.cli.Command.createCliCommand(clazz);
38
43
  cmd.set({
@@ -420,14 +425,30 @@ qx.Class.define("qx.tool.compiler.cli.commands.Compile", {
420
425
  properties: {},
421
426
 
422
427
  members: {
428
+ /** @type{cliProgress.SingleBar|null} progress bar instance */
423
429
  __progressBar: null,
430
+ /** @type{qx.tool.compiler.makers.Maker[]|null} list of makers created from config */
424
431
  __makers: null,
432
+ /** @type{Object} map of namespace to Library instance */
425
433
  __libraries: null,
434
+ /** @type{Boolean} true if the output directory was created during this run */
426
435
  __outputDirWasCreated: false,
427
436
  /** @type {Boolean} Whether libraries have had their `.load()` method called yet */
428
437
  __librariesNotified: false,
429
438
 
430
- /*
439
+ /** @type{String} the path to the root of the meta files by classname */
440
+ __metaDir: null,
441
+
442
+ /** @type{Boolean} whether the typescript output is enabled */
443
+ __typescriptEnabled: false,
444
+
445
+ /** @type{String} the name of the typescript file to generate, null = use default */
446
+ __typescriptFile: null,
447
+
448
+ /** @type{Boolean} whether the typescript watcher has already been attached (watch mode) */
449
+ __typescriptWatcherAttached: false,
450
+
451
+ /**
431
452
  * @Override
432
453
  */
433
454
  async process() {
@@ -830,17 +851,36 @@ Framework: v${await this.getQxVersion()} in ${await this.getQxPath()}`);
830
851
  );
831
852
 
832
853
  watch.setConfigFilenames(arr);
854
+
855
+ if (this.__typescriptEnabled && target instanceof qx.tool.compiler.targets.SourceTarget && !this.__typescriptWatcherAttached) {
856
+ this.__typescriptWatcherAttached = true;
857
+ try {
858
+ await this.__attachTypescriptWatcher(watch);
859
+ } catch (ex) {
860
+ qx.tool.compiler.Console.error(ex);
861
+ }
862
+ }
863
+
833
864
  waiters.push(watch.start());
834
865
  }
835
866
  }
867
+
868
+ if (!this.argv.watch && this.__typescriptEnabled) {
869
+ try {
870
+ await this.__attachTypescriptWatcher(null);
871
+ } catch (ex) {
872
+ qx.tool.compiler.Console.error(ex);
873
+ }
874
+ }
875
+
836
876
  return qx.Promise.all(waiters);
837
877
  },
838
878
 
839
879
  /**
840
- * Processes the configuration from a JSON data structure and creates a Maker
880
+ * Processes the configuration from a JSON data structure and creates Makers
841
881
  *
842
- * @param data {Map}
843
- * @return {Maker}
882
+ * @param data {Object} the compile.json configuration data
883
+ * @return {Promise<qx.tool.compiler.makers.Maker[]>}
844
884
  */
845
885
  async createMakersFromConfig(data) {
846
886
  const Console = qx.tool.compiler.Console.getInstance();
@@ -860,6 +900,16 @@ Framework: v${await this.getQxVersion()} in ${await this.getQxPath()}`);
860
900
  delete data.babelOptions;
861
901
  }
862
902
 
903
+ if (qx.lang.Type.isBoolean(data?.meta?.typescript)) {
904
+ this.__typescriptEnabled = data.meta.typescript;
905
+ } else if (qx.lang.Type.isString(data?.meta?.typescript)) {
906
+ this.__typescriptEnabled = true;
907
+ this.__typescriptFile = path.relative(process.cwd(), path.resolve(data.meta.typescript));
908
+ }
909
+ if (this.argv.typescript === true) {
910
+ this.__typescriptEnabled = true;
911
+ }
912
+
863
913
  var argvAppNames = null;
864
914
  if (t.argv["app-name"]) {
865
915
  argvAppNames = {};
@@ -1049,6 +1099,14 @@ Framework: v${await this.getQxVersion()} in ${await this.getQxPath()}`);
1049
1099
  }
1050
1100
  });
1051
1101
 
1102
+ this.__metaDir = data.meta?.output;
1103
+ if (!this.__metaDir && targetConfigs.length > 0) {
1104
+ this.__metaDir = path.relative(
1105
+ process.cwd(),
1106
+ path.resolve(targetConfigs[0].outputPath, "../meta")
1107
+ );
1108
+ }
1109
+
1052
1110
  /*
1053
1111
  * There is still only one target per maker, so convert our list of targetConfigs into an array of makers
1054
1112
  */
@@ -1516,6 +1574,80 @@ Framework: v${await this.getQxVersion()} in ${await this.getQxPath()}`);
1516
1574
  return makers;
1517
1575
  },
1518
1576
 
1577
+ /**
1578
+ * Loads class metadata, generates TypeScript definitions, and (in watch mode) sets up a
1579
+ * debounced file watcher that re-runs metadata parsing and TypeScript generation whenever
1580
+ * a source file changes. When `watch` is null the method performs a single one-shot run
1581
+ * and returns immediately after writing the output.
1582
+ *
1583
+ * @param watch {qx.tool.compiler.cli.Watch|null} watcher instance in watch mode, or null for a one-shot compile
1584
+ */
1585
+ async __attachTypescriptWatcher(watch) {
1586
+ qx.tool.compiler.Console.info(`Loading meta data ...`);
1587
+ let metaDb = new qx.tool.compiler.MetaDatabase().set({ rootDir: this.__metaDir });
1588
+ await metaDb.load(); // hydrates existing class metadata from disk; library map is rebuilt fresh below
1589
+
1590
+ metaDb.getDatabase().libraries = {};
1591
+ const dirs = [];
1592
+ for (let lib of Object.values(this.__libraries)) {
1593
+ let dir = path.join(lib.getRootDir(), lib.getSourcePath());
1594
+ metaDb.getDatabase().libraries[lib.getNamespace()] = { sourceDir: dir };
1595
+ dirs.push(dir);
1596
+ }
1597
+ await metaDb.loadFromDirectories(dirs, { force: !!this.argv.clean });
1598
+ await metaDb.save();
1599
+
1600
+ let tsWriter = null;
1601
+ if (this.__typescriptEnabled) {
1602
+ qx.tool.compiler.Console.info(`Generating typescript output ...`);
1603
+ tsWriter = new qx.tool.compiler.targets.TypeScriptWriter(metaDb);
1604
+ if (this.__typescriptFile) {
1605
+ tsWriter.setOutputTo(this.__typescriptFile);
1606
+ } else {
1607
+ tsWriter.setOutputTo(path.join(this.__metaDir, "..", "qooxdoo.d.ts"));
1608
+ }
1609
+ await tsWriter.process();
1610
+ }
1611
+
1612
+ if (!watch) {
1613
+ return;
1614
+ }
1615
+
1616
+ // Watch mode: re-run metadata and typescript on file changes
1617
+ let classFiles = {};
1618
+ let debounce = new qx.tool.utils.Debounce(async () => {
1619
+ let filesParsed = false;
1620
+ qx.tool.compiler.Console.info(`Loading meta data ...`);
1621
+ let addFilePromises = [];
1622
+ let arr = Object.keys(classFiles);
1623
+ if (arr.length > 0) {
1624
+ filesParsed = true;
1625
+ classFiles = {};
1626
+ arr.forEach(filename => {
1627
+ addFilePromises.push(metaDb.addFile(filename));
1628
+ });
1629
+ }
1630
+ if (filesParsed) {
1631
+ qx.tool.compiler.Console.info(`Generating typescript output ...`);
1632
+ await Promise.all(addFilePromises);
1633
+ await metaDb.reparseAll();
1634
+ await metaDb.save();
1635
+ if (this.__typescriptEnabled) {
1636
+ await tsWriter.process();
1637
+ }
1638
+ }
1639
+ });
1640
+
1641
+ watch.addListener("fileChanged", evt => {
1642
+ let data = evt.getData();
1643
+ if (data.fileType == "source") {
1644
+ let filename = data.library.getFilename(data.filename);
1645
+ classFiles[filename] = true;
1646
+ debounce.run();
1647
+ }
1648
+ });
1649
+ },
1650
+
1519
1651
  /**
1520
1652
  * Checks the dependencies of the current library
1521
1653
  * @param {qx.tool.compiler.app.Library[]} libs
@@ -1663,10 +1795,9 @@ Framework: v${await this.getQxVersion()} in ${await this.getQxPath()}`);
1663
1795
  },
1664
1796
 
1665
1797
  /**
1666
- * Resolves the target class instance from the type name; accepts "source" or "build" or
1667
- * a class name
1798
+ * Resolves the target class from the type name; accepts "source", "build", or a class
1668
1799
  * @param type {String}
1669
- * @returns {Maker}
1800
+ * @returns {Function|null}
1670
1801
  */
1671
1802
  __resolveTargetClass(type) {
1672
1803
  if (!type) {
@@ -1711,7 +1842,7 @@ Framework: v${await this.getQxVersion()} in ${await this.getQxPath()}`);
1711
1842
  * Returns the makers for a given application name
1712
1843
  *
1713
1844
  * @param appName {String} the name of the application
1714
- * @return {Maker}
1845
+ * @return {qx.tool.compiler.makers.Maker[]}
1715
1846
  */
1716
1847
  getMakersForApp(appName) {
1717
1848
  return this.__makers.filter(maker => {
@@ -18,7 +18,6 @@
18
18
  ************************************************************************ */
19
19
  const fs = require("fs");
20
20
  const path = require("upath");
21
- const inquirer = require("inquirer");
22
21
 
23
22
  /**
24
23
  * Create a new qooxdoo project. This will assemble the information needed to create the
@@ -210,6 +209,7 @@ qx.Class.define("qx.tool.compiler.cli.commands.Create", {
210
209
  // ask user for missing values
211
210
  let answers;
212
211
  try {
212
+ const { default: inquirer } = await import("inquirer");
213
213
  answers = await inquirer.prompt(questions);
214
214
  } catch (e) {
215
215
  throw new qx.tool.utils.Utils.UserError(e.message);
@@ -209,7 +209,7 @@ qx.Class.define("qx.tool.compiler.cli.commands.Serve", {
209
209
  });
210
210
  }
211
211
  let config = this.getCompilerApi().getConfiguration();
212
- let listenPort = config?.serve?.listenPort ?? this.argv.listenPort;
212
+ let listenPort = this.argv.listenPort ?? config?.serve?.listenPort;
213
213
  let server = http.createServer(app);
214
214
  this.fireDataEvent("beforeStart", {
215
215
  server: server,
@@ -91,32 +91,6 @@ qx.Class.define("qx.tool.compiler.cli.commands.Typescript", {
91
91
  ig.add(exclude);
92
92
  }
93
93
 
94
- const classFiles = [];
95
-
96
- const scanImpl = async filename => {
97
- let basename = path.basename(filename);
98
- let stat = await fs.promises.stat(filename);
99
-
100
- // Check if this file/directory should be ignored
101
- let relativePath = path.relative(process.cwd(), filename);
102
- if (ig.ignores(relativePath)) {
103
- return;
104
- }
105
-
106
- if (stat.isFile() && basename.match(/\.js$/)) {
107
- classFiles.push(filename);
108
- } else if (
109
- stat.isDirectory() &&
110
- (basename == "." || basename[0] != ".")
111
- ) {
112
- let files = await fs.promises.readdir(filename);
113
- for (let i = 0; i < files.length; i++) {
114
- let subname = path.join(filename, files[i]);
115
- await scanImpl(subname);
116
- }
117
- }
118
- };
119
-
120
94
  let files = this.argv.files || [];
121
95
  if (files.length === 0) {
122
96
  if (fs.existsSync("Manifest.json")) {
@@ -137,18 +111,37 @@ qx.Class.define("qx.tool.compiler.cli.commands.Typescript", {
137
111
  qx.tool.compiler.Console.error("No files to process");
138
112
  process.exit(1);
139
113
  }
140
- for (let file of files) {
141
- await scanImpl(file);
142
- }
143
114
 
144
115
  if (qx.core.Environment.get("qx.debug")) {
145
116
  if (this.argv.metaDebug) {
146
117
  this.argv.verbose = true;
147
- if (classFiles.length > 1) {
148
- console.log("Only one file can be processed in meta debug mode");
118
+ let target = files[0];
119
+ let stat = await fs.promises.stat(target);
120
+ if (stat.isDirectory()) {
121
+ const findFirst = async dir => {
122
+ for (let entry of await fs.promises.readdir(dir)) {
123
+ let full = path.join(dir, entry);
124
+ let s = await fs.promises.stat(full);
125
+ if (s.isFile() && entry.endsWith(".js")) {
126
+ return full;
127
+ }
128
+ if (s.isDirectory() && entry[0] !== ".") {
129
+ let found = await findFirst(full);
130
+ if (found) {
131
+ return found;
132
+ }
133
+ }
134
+ }
135
+ return null;
136
+ };
137
+ target = await findFirst(target);
138
+ if (!target) {
139
+ qx.tool.compiler.Console.error("No .js file found for meta debug");
140
+ process.exit(1);
141
+ }
149
142
  }
150
143
  let meta = new qx.tool.compiler.MetaExtraction();
151
- await meta.parse(classFiles[0]);
144
+ await meta.parse(target);
152
145
  meta.fixupJsDoc({ resolveType: type => type });
153
146
  console.log(JSON.stringify(meta.getMetaData(), null, 2));
154
147
  process.exit(0);
@@ -157,13 +150,7 @@ qx.Class.define("qx.tool.compiler.cli.commands.Typescript", {
157
150
 
158
151
  let metaDb = new qx.tool.compiler.MetaDatabase();
159
152
  await metaDb.load();
160
- for (let filename of classFiles) {
161
- if (this.argv.verbose) {
162
- qx.tool.compiler.Console.info(`Processing ${filename} ...`);
163
- }
164
- await metaDb.addFile(filename);
165
- }
166
- await metaDb.reparseAll();
153
+ await metaDb.loadFromDirectories(files, { ignore: ig, verbose: this.argv.verbose });
167
154
 
168
155
  let tsWriter = new qx.tool.compiler.targets.TypeScriptWriter(metaDb);
169
156
  if (this.argv.outputFilename) {
@@ -20,7 +20,6 @@
20
20
  const fs = require("fs");
21
21
  const process = require("process");
22
22
  const path = require("upath");
23
- const inquirer = require("inquirer");
24
23
  /**
25
24
  * Add a new script file to the current project, to be loaded by the qooxdoo boot loader
26
25
  *
@@ -125,6 +124,7 @@ qx.Class.define("qx.tool.compiler.cli.commands.add.Script", {
125
124
  }
126
125
  if ((await fs.existsAsync(resource_file_path)) && !this.argv.undo) {
127
126
  if (!this.argv.noninteractive) {
127
+ const { default: inquirer } = await import("inquirer");
128
128
  let question = {
129
129
  type: "confirm",
130
130
  name: "doOverwrite",
@@ -20,7 +20,6 @@ const path = require("upath");
20
20
  const process = require("process");
21
21
  const { Octokit } = require("@octokit/rest");
22
22
  const semver = require("semver");
23
- const inquirer = require("inquirer");
24
23
  const { glob } = require("glob");
25
24
 
26
25
  /**
@@ -148,6 +147,7 @@ qx.Class.define("qx.tool.compiler.cli.commands.package.Publish", {
148
147
  async process() {
149
148
  // init
150
149
  const argv = this.argv;
150
+ const { default: inquirer } = await import("inquirer");
151
151
 
152
152
  // qooxdoo version
153
153
  let qxVersion = await this.getQxVersion();
@@ -521,6 +521,7 @@ qx.Class.define("qx.tool.compiler.cli.commands.package.Publish", {
521
521
  * @private
522
522
  */
523
523
  async __createIndexFile(argv) {
524
+ const { default: inquirer } = await import("inquirer");
524
525
  if (argv.verbose && !argv.quiet) {
525
526
  qx.tool.compiler.Console.info("Creating index file...");
526
527
  }
@@ -552,7 +553,7 @@ qx.Class.define("qx.tool.compiler.cli.commands.package.Publish", {
552
553
  let answer = await inquirer.prompt({
553
554
  name: "mainpath",
554
555
  message: "Please choose the main library",
555
- type: "list",
556
+ type: "select",
556
557
  choices
557
558
  });
558
559
 
@@ -18,13 +18,13 @@
18
18
  const process = require("process");
19
19
  const { Octokit } = require("@octokit/rest");
20
20
  const semver = require("semver");
21
- const inquirer = require("inquirer");
22
21
  const path = require("upath");
23
22
 
24
23
  /**
25
24
  * Updates the local cache with information of available library packages
26
25
  *
27
26
  * @ignore(Buffer.from)
27
+ * @ignore(fetch)
28
28
  */
29
29
  qx.Class.define("qx.tool.compiler.cli.commands.package.Update", {
30
30
  extend: qx.tool.compiler.cli.commands.Package,
@@ -120,6 +120,7 @@ qx.Class.define("qx.tool.compiler.cli.commands.package.Update", {
120
120
  await this.updateFromRepository();
121
121
  } else {
122
122
  if (!github.token) {
123
+ const { default: inquirer } = await import("inquirer");
123
124
  let response = await inquirer.prompt([
124
125
  {
125
126
  type: "input",
@@ -173,7 +174,6 @@ qx.Class.define("qx.tool.compiler.cli.commands.package.Update", {
173
174
  }
174
175
  let url = this.getRepositoryCacheUrl();
175
176
  try {
176
- let fetch = (await import("node-fetch")).default;
177
177
  let res = await fetch(url);
178
178
  let data = await res.json();
179
179
  this.setCache(data);
@@ -440,6 +440,9 @@ qx.Class.define("qx.tool.compiler.targets.TypeScriptWriter", {
440
440
 
441
441
  if (typeof typename == "object") {
442
442
  if ("type" in typename) {
443
+ if (!typename.type) {
444
+ return defaultType;
445
+ }
443
446
  const dimensions = typename.dimensions ?? 1;
444
447
  typename = typename.type + "[]".repeat(dimensions - 1);
445
448
  } else {
@@ -80,7 +80,27 @@ qx.Class.define("qx.tool.compiler.targets.meta.Browserify", {
80
80
  async writeToDisk() {
81
81
  const localModules = this.getAppMeta().getApplication().getLocalModules();
82
82
  let db = this.getAppMeta().getAnalyser().getDatabase();
83
- const { commonjsModules } = this.__getCommonjsModules();
83
+ const { commonjsModules, references } = this.__getCommonjsModules();
84
+
85
+ // Warn about missing npm modules on every compile, not just when the bundle is rebuilt
86
+ if (this.getAppMeta().getEnvironmentValue("qx.compiler.applicationType") == "browser") {
87
+ for (const moduleName of commonjsModules) {
88
+ try {
89
+ require.resolve(moduleName, { paths: [process.cwd()] });
90
+ } catch (_) {
91
+ const msg = [`WARNING: could not locate require()d module: "${moduleName}"`, " required from:"];
92
+ const modRefs = references[moduleName];
93
+ if (modRefs) {
94
+ for (const mr of modRefs) {
95
+ for (const ref of mr) {
96
+ msg.push(" " + ref);
97
+ }
98
+ }
99
+ }
100
+ qx.tool.compiler.Console.error(msg.join("\n"));
101
+ }
102
+ }
103
+ }
84
104
 
85
105
  let modules = [];
86
106
  let modulesInfo = {};
@@ -147,91 +167,133 @@ qx.Class.define("qx.tool.compiler.targets.meta.Browserify", {
147
167
  });
148
168
  },
149
169
 
150
- __browserify(commonjsModules, references, localModules, ws) {
151
- const babelify = require("babelify");
152
- const preset = require("@babel/preset-env");
153
- const browserify = require("browserify");
154
- const builtins = require("browserify/lib/builtins.js");
155
-
156
- // For some reason, `process` is not require()able, but `_process` is.
157
- // Make them equivalent.
158
- builtins.process = builtins._process;
159
-
160
- return new Promise((resolve, reject) => {
161
- const options = {
162
- builtins: builtins,
163
- ignoreMissing: true,
164
- insertGlobals: true,
165
- detectGlobals: true
166
- };
167
- qx.lang.Object.mergeWith(
168
- options,
169
- this.getAppMeta().getAnalyser().getBrowserifyConfig()?.options || {},
170
- false
171
- );
172
- let b = browserify([], options);
170
+ async __browserify(commonjsModules, references, localModules, ws) {
171
+ const esbuild = require("esbuild");
172
+ const { polyfillNode } = require("esbuild-plugin-polyfill-node");
173
173
 
174
- b._mdeps.on("missing", (id, parent) => {
175
- let message = [];
176
- message.push(`ERROR: could not locate require()d module: "${id}"`);
177
- message.push(" required from:");
178
- try {
179
- [...references[id]].forEach(refs => {
180
- refs.forEach(ref => {
181
- message.push(` ${ref}`);
182
- });
183
- });
184
- } catch (e) {
185
- message.push(` <compile.json:application.localModules'>`);
186
- }
187
- qx.tool.compiler.Console.error(message.join("\n"));
188
- });
174
+ // Convert a module name to a valid JS identifier
175
+ const safeName = name => "_m_" + name.replace(/[^a-zA-Z0-9_$]/g, "_");
189
176
 
190
- // Include any dynamically determined `require()`d modules
191
- if (commonjsModules.length > 0) {
192
- b.require(commonjsModules);
177
+ // esbuild equivalent of browserify's ignoreMissing:true — logs a warning and
178
+ // returns an empty module stub so the bundle is still produced
179
+ const missingModulePlugin = {
180
+ name: "qx-missing-module",
181
+ setup(build) {
182
+ const onResolveHandler = async args => {
183
+ if (args.namespace === "qx-missing") { return null; }
184
+ const searchPaths = [args.resolveDir, process.cwd()].filter(Boolean);
185
+ for (const dir of searchPaths) {
186
+ try {
187
+ require.resolve(args.path, { paths: [dir] });
188
+ return null;
189
+ } catch (_) {}
190
+ }
191
+ return { path: args.path, namespace: "qx-missing" };
192
+ };
193
+ build.onResolve({ filter: /^[^./]/ }, onResolveHandler);
194
+ build.onLoad({ filter: /.*/, namespace: "qx-missing" }, args => ({
195
+ contents: `// Missing module: ${args.path}\nmodule.exports = {};`,
196
+ loader: "js"
197
+ }));
193
198
  }
199
+ };
194
200
 
195
- // Include any local modules specified for the application
196
- // in compile.json
197
- if (localModules) {
198
- for (let requireName in localModules) {
199
- b.require(localModules[requireName], { expose: requireName });
200
- }
201
+ const allModules = [
202
+ ...commonjsModules.map(m => ({ require: m, file: m })),
203
+ ...Object.entries(localModules || {}).map(([name, file]) => ({ require: name, file }))
204
+ ];
205
+
206
+ // Build a virtual entry that imports all required modules and exposes
207
+ // them via a global require() function compatible with Qooxdoo's runtime require() calls
208
+ const entryContent = [
209
+ ...allModules.map(m => `import * as ${safeName(m.require)} from ${JSON.stringify(m.file)};`),
210
+ `const __qx_mods = {`,
211
+ ...allModules.map(m => ` ${JSON.stringify(m.require)}: ${safeName(m.require)},`),
212
+ `};`,
213
+ `const __prev = typeof globalThis.require === "function" ? globalThis.require : null;`,
214
+ `globalThis.require = function(name) {`,
215
+ ` if (name in __qx_mods) return __qx_mods[name];`,
216
+ ` if (__prev) return __prev(name);`,
217
+ ` throw new Error("Module not found: " + name);`,
218
+ `};`
219
+ ].join("\n");
220
+
221
+ // Merge in any user-provided esbuild options from compile.json's "browserify" key
222
+ const browserifyConfig = this.getAppMeta().getAnalyser().getBrowserifyConfig() || {};
223
+ const userOptions = { ...(browserifyConfig.options || {}) };
224
+
225
+ // Filter out browserify-only options that esbuild doesn't understand
226
+ const BROWSERIFY_ONLY_OPTS = ["noParse", "ignoreMissing", "insertGlobals", "detectGlobals", "builtins"];
227
+ for (const opt of BROWSERIFY_ONLY_OPTS) {
228
+ if (opt in userOptions) {
229
+ qx.tool.compiler.Console.warn(
230
+ `WARNING: compile.json browserify.options.${opt} is a browserify-only option and is not supported by esbuild — it will be ignored.` +
231
+ (opt === "noParse" ? ' Use esbuild\'s "external" option instead if needed.' : "")
232
+ );
233
+ delete userOptions[opt];
201
234
  }
202
- // Ensure ES6 local modules are converted to CommonJS format
203
- b.transform(babelify, {
204
- presets: [preset],
205
- sourceMaps: false,
206
- global: true
207
- });
208
-
209
- b.bundle(function (e, output) {
210
- if (e) {
211
- // THIS IS A HACK!
212
- // In case of error dependency walker never returns from
213
- // ```if (self.inputPending > 0) return setTimeout(resolve);```
214
- // because inputPending is not decremented anymore.
215
- // so set it to 0 here.
216
- let d = b.pipeline.get("deps");
217
- d.get(0).inputPending = 0;
218
- qx.tool.compiler.Console.error(
219
- `Unable to browserify - this is probably because a module is being require()'d which is not compatible with Browserify:\n${e.message}`
220
- );
221
-
222
- setTimeout(() => {
223
- this.emit("end");
224
- });
225
- return;
235
+ }
236
+
237
+ // polyfillNode is enabled by default; set browserify.polyfillNode: false in compile.json to disable
238
+ const usePolyfillNode = browserifyConfig.polyfillNode !== false;
239
+
240
+ // Extract define and plugins manually (object rest destructuring is not supported by the qx parser)
241
+ const userDefine = userOptions.define || {};
242
+ const userPlugins = Array.isArray(userOptions.plugins) ? userOptions.plugins : [];
243
+ delete userOptions.define;
244
+ delete userOptions.plugins;
245
+ const basePlugins = usePolyfillNode ? [polyfillNode()] : [];
246
+
247
+ let result;
248
+ try {
249
+ result = await esbuild.build(
250
+ Object.assign(
251
+ {
252
+ stdin: {
253
+ contents: entryContent,
254
+ resolveDir: process.cwd()
255
+ },
256
+ bundle: true,
257
+ platform: "browser",
258
+ format: "iife",
259
+ write: false,
260
+ sourcemap: false,
261
+ logLevel: "silent",
262
+ // Preserve .name property of classes/functions even when esbuild renames bindings to
263
+ // avoid scope conflicts inside already-bundled packages (e.g. browserified bundles that
264
+ // have duplicate identifier names across inlined modules).
265
+ keepNames: true,
266
+ // Replace global with globalThis so Node.js packages using `global.<x>` work in browsers
267
+ define: Object.assign({ global: "globalThis" }, userDefine),
268
+ plugins: basePlugins.concat([missingModulePlugin], userPlugins)
269
+ },
270
+ userOptions
271
+ )
272
+ );
273
+ } catch (e) {
274
+ // Report missing/unresolvable modules with context from the analyser database
275
+ for (const err of e.errors || []) {
276
+ const id = err.text?.match(/Cannot resolve "([^"]+)"/)?.[1] || err.text;
277
+ let message = [`ERROR: could not bundle module: "${id || err.text}"`];
278
+ if (id && references[id]) {
279
+ message.push(" required from:");
280
+ try {
281
+ for (const refs of references[id]) {
282
+ for (const ref of refs) {
283
+ message.push(" " + ref);
284
+ }
285
+ }
286
+ } catch (_) {}
226
287
  }
227
- // in case of end event output can not be writen.
228
- // so catch the error and ignore it
229
- try {
230
- ws.write(output);
231
- } catch (err) {}
232
- resolve(null);
233
- });
234
- });
288
+ qx.tool.compiler.Console.error(message.join("\n"));
289
+ }
290
+ qx.tool.compiler.Console.error(`Unable to bundle CommonJS modules: ${e.message}`);
291
+ return;
292
+ }
293
+
294
+ try {
295
+ ws.write(Buffer.from(result.outputFiles[0].contents));
296
+ } catch (_) {}
235
297
  },
236
298
 
237
299
  /**