pxt-core 11.1.4 → 11.1.5

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/built/cli.js CHANGED
@@ -2520,6 +2520,7 @@ function serveAsync(parsed) {
2520
2520
  browser: parsed.flags["browser"],
2521
2521
  serial: !parsed.flags["noSerial"] && !exports.globalConfig.noSerial,
2522
2522
  noauth: parsed.flags["noauth"] || false,
2523
+ backport: parsed.flags["backport"] || 0,
2523
2524
  }));
2524
2525
  }
2525
2526
  exports.serveAsync = serveAsync;
@@ -6237,6 +6238,11 @@ ${pxt.crowdin.KEY_VARIABLE} - crowdin key
6237
6238
  noauth: {
6238
6239
  description: "disable localtoken-based authentication",
6239
6240
  aliases: ["na"],
6241
+ },
6242
+ backport: {
6243
+ description: "port where the locally running backend is listening.",
6244
+ argument: "backport",
6245
+ type: "number",
6240
6246
  }
6241
6247
  }
6242
6248
  }, serveAsync);
package/built/pxt.js CHANGED
@@ -102515,7 +102515,7 @@ var pxt;
102515
102515
  var _a;
102516
102516
  try {
102517
102517
  return typeof window !== "undefined"
102518
- && /^http:\/\/(localhost|127\.0\.0\.1|192\.168\.\d+\.\d+):\d+\//.test(window.location.href)
102518
+ && /^http:\/\/(?:localhost|127\.0\.0\.1|192\.168\.\d{1,3}\.\d{1,3}|[a-zA-Z0-9.-]+\.local):\d+\/?/.test(window.location.href)
102519
102519
  && (ignoreFlags || !/nolocalhost=1/.test(window.location.href))
102520
102520
  && !((_a = pxt === null || pxt === void 0 ? void 0 : pxt.webConfig) === null || _a === void 0 ? void 0 : _a.isStatic);
102521
102521
  }
@@ -153913,11 +153913,13 @@ var pxsim;
153913
153913
  return isPxtElectron() || isIpcRenderer();
153914
153914
  }
153915
153915
  U.isElectron = isElectron;
153916
+ function testLocalhost(url) {
153917
+ return /^http:\/\/(?:localhost|127\.0\.0\.1|192\.168\.\d{1,3}\.\d{1,3}|[a-zA-Z0-9.-]+\.local):\d+\/?/.test(url) && !/nolocalhost=1/.test(url);
153918
+ }
153919
+ U.testLocalhost = testLocalhost;
153916
153920
  function isLocalHost() {
153917
153921
  try {
153918
- return typeof window !== "undefined"
153919
- && /^http:\/\/(localhost|127\.0\.0\.1):\d+\//.test(window.location.href)
153920
- && !/nolocalhost=1/.test(window.location.href);
153922
+ return typeof window !== "undefined" && testLocalhost(window.location.href);
153921
153923
  }
153922
153924
  catch (e) {
153923
153925
  return false;
@@ -153941,6 +153943,14 @@ var pxsim;
153941
153943
  return v;
153942
153944
  }
153943
153945
  U.unique = unique;
153946
+ function sanitizeCssName(name) {
153947
+ let sanitized = name.replace(/[^a-zA-Z0-9-_]/g, '_');
153948
+ if (!/^[a-zA-Z_]/.test(sanitized)) {
153949
+ sanitized = 'cls_' + sanitized;
153950
+ }
153951
+ return sanitized;
153952
+ }
153953
+ U.sanitizeCssName = sanitizeCssName;
153944
153954
  })(U = pxsim.U || (pxsim.U = {}));
153945
153955
  class BreakLoopException {
153946
153956
  }
@@ -155317,16 +155327,42 @@ var pxsim;
155317
155327
  this._allowedOrigins.push(options.parentOrigin);
155318
155328
  }
155319
155329
  this._allowedOrigins.push(this.getSimUrl().origin);
155320
- const messageSimulators = options === null || options === void 0 ? void 0 : options.messageSimulators;
155321
- if (messageSimulators) {
155322
- Object.keys(messageSimulators)
155323
- .map(channel => messageSimulators[channel])
155324
- .forEach(messageSimulator => {
155325
- this._allowedOrigins.push(new URL(messageSimulator.url).origin);
155326
- if (messageSimulator.localHostUrl)
155327
- this._allowedOrigins.push(new URL(messageSimulator.localHostUrl).origin);
155328
- });
155329
- }
155330
+ // Legacy support for message simulators
155331
+ const messageSimulators = (options === null || options === void 0 ? void 0 : options.messageSimulators) || {};
155332
+ Object.keys(messageSimulators)
155333
+ .map(channel => messageSimulators[channel])
155334
+ .forEach(messageSimulator => {
155335
+ this._allowedOrigins.push(new URL(messageSimulator.url).origin);
155336
+ if (messageSimulator.localHostUrl)
155337
+ this._allowedOrigins.push(new URL(messageSimulator.localHostUrl).origin);
155338
+ });
155339
+ // Preprocess simulator extensions
155340
+ const simXDevMode = pxsim.U.isLocalHost() && /[?&]simxdev(?:[=&#]|$)/i.test(window.location.href);
155341
+ Object.entries((options === null || options === void 0 ? void 0 : options.simulatorExtensions) || {}).forEach(([key, simx]) => {
155342
+ // Verify essential `simx` config was provided
155343
+ if (!simx ||
155344
+ !simx.index ||
155345
+ !simx.aspectRatio ||
155346
+ simx.permanent === undefined) {
155347
+ return;
155348
+ }
155349
+ // Compute the effective URL
155350
+ if (simXDevMode && simx.devUrl) {
155351
+ // Use the dev URL if the dev flag is set (and we're on localhost)
155352
+ simx.url = new URL(simx.index, simx.devUrl).toString();
155353
+ }
155354
+ else {
155355
+ const simUrl = this.getSimUrl();
155356
+ // Ensure we preserve upload target path (/app/<sha>---simulator)
155357
+ const simPath = simUrl.pathname.replace(/---?.*/, "");
155358
+ // Construct the path. The "-" element delineates the extension key from the resource name.
155359
+ const simxPath = [simPath, "simx", key, "-", simx.index].join("/");
155360
+ // Create the fully-qualified URL, preserving the origin by removing all leading slashes
155361
+ simx.url = new URL(simxPath.replace(/^\/+/, ""), simUrl.origin).toString();
155362
+ }
155363
+ // Add the origin to the allowed origins
155364
+ this._allowedOrigins.push(new URL(simx.url).origin);
155365
+ });
155330
155366
  this._allowedOrigins = pxsim.U.unique(this._allowedOrigins, f => f);
155331
155367
  }
155332
155368
  isDebug() {
@@ -155533,8 +155569,44 @@ var pxsim;
155533
155569
  const messageSimulator = messageChannel &&
155534
155570
  this.options.messageSimulators &&
155535
155571
  this.options.messageSimulators[messageChannel];
155536
- // should we start an extension editor?
155537
- if (messageSimulator) {
155572
+ const simulatorExtension = messageChannel &&
155573
+ this.options.simulatorExtensions &&
155574
+ this.options.simulatorExtensions[messageChannel];
155575
+ const startSimulatorExtension = (url, permanent, aspectRatio) => {
155576
+ var _a;
155577
+ aspectRatio = aspectRatio || ((_a = this._runOptions) === null || _a === void 0 ? void 0 : _a.aspectRatio) || 1.22;
155578
+ let wrapper = this.createFrame(url);
155579
+ this.container.appendChild(wrapper);
155580
+ const messageFrame = wrapper.firstElementChild;
155581
+ messageFrame.dataset[FRAME_DATA_MESSAGE_CHANNEL] = messageChannel;
155582
+ messageFrame.dataset[FRAME_ASPECT_RATIO] = aspectRatio + "";
155583
+ pxsim.U.addClass(wrapper, "simmsg");
155584
+ pxsim.U.addClass(wrapper, "simmsg" + pxsim.U.sanitizeCssName(messageChannel));
155585
+ if (permanent)
155586
+ messageFrame.dataset[PERMANENT] = "true";
155587
+ this.startFrame(messageFrame);
155588
+ frames = this.simFrames(); // refresh
155589
+ };
155590
+ // should we start a simulator extension for this message?
155591
+ if (simulatorExtension) {
155592
+ // find a frame already running that simulator
155593
+ let messageFrame = frames.find(frame => frame.dataset[FRAME_DATA_MESSAGE_CHANNEL] === messageChannel);
155594
+ // not found, spin a new one
155595
+ if (!messageFrame) {
155596
+ const url = new URL(simulatorExtension.url);
155597
+ if (this.options.parentOrigin)
155598
+ url.searchParams.set("parentOrigin", encodeURIComponent(this.options.parentOrigin));
155599
+ if (this.options.userLanguage)
155600
+ url.searchParams.set("language", encodeURIComponent(this.options.userLanguage));
155601
+ startSimulatorExtension(url.toString(), simulatorExtension.permanent, simulatorExtension.aspectRatio);
155602
+ }
155603
+ // not running the current run, restart
155604
+ else if (messageFrame.dataset['runid'] != this.runId) {
155605
+ this.startFrame(messageFrame);
155606
+ }
155607
+ }
155608
+ // (legacy: messageSimulator) should we start a message simulator for this message?
155609
+ else if (messageSimulator) {
155538
155610
  // find a frame already running that simulator
155539
155611
  let messageFrame = frames.find(frame => frame.dataset[FRAME_DATA_MESSAGE_CHANNEL] === messageChannel);
155540
155612
  // not found, spin a new one
@@ -155543,16 +155615,7 @@ var pxsim;
155543
155615
  const url = ((useLocalHost && messageSimulator.localHostUrl) || messageSimulator.url)
155544
155616
  .replace("$PARENT_ORIGIN$", encodeURIComponent(this.options.parentOrigin || ""))
155545
155617
  .replace("$LANGUAGE$", encodeURIComponent(this.options.userLanguage));
155546
- let wrapper = this.createFrame(url);
155547
- this.container.appendChild(wrapper);
155548
- messageFrame = wrapper.firstElementChild;
155549
- messageFrame.dataset[FRAME_DATA_MESSAGE_CHANNEL] = messageChannel;
155550
- pxsim.U.addClass(wrapper, "simmsg");
155551
- pxsim.U.addClass(wrapper, "simmsg" + messageChannel);
155552
- if (messageSimulator.permanent)
155553
- messageFrame.dataset[PERMANENT] = "true";
155554
- this.startFrame(messageFrame);
155555
- frames = this.simFrames(); // refresh
155618
+ startSimulatorExtension(url, messageSimulator.permanent, messageSimulator.aspectRatio);
155556
155619
  }
155557
155620
  // not running the curren run, restart
155558
155621
  else if (messageFrame.dataset['runid'] != this.runId) {
@@ -163160,6 +163223,7 @@ function serveAsync(parsed) {
163160
163223
  browser: parsed.flags["browser"],
163161
163224
  serial: !parsed.flags["noSerial"] && !exports.globalConfig.noSerial,
163162
163225
  noauth: parsed.flags["noauth"] || false,
163226
+ backport: parsed.flags["backport"] || 0,
163163
163227
  }));
163164
163228
  }
163165
163229
  exports.serveAsync = serveAsync;
@@ -166877,6 +166941,11 @@ ${pxt.crowdin.KEY_VARIABLE} - crowdin key
166877
166941
  noauth: {
166878
166942
  description: "disable localtoken-based authentication",
166879
166943
  aliases: ["na"],
166944
+ },
166945
+ backport: {
166946
+ description: "port where the locally running backend is listening.",
166947
+ argument: "backport",
166948
+ type: "number",
166880
166949
  }
166881
166950
  }
166882
166951
  }, serveAsync);
package/built/pxtlib.js CHANGED
@@ -4829,7 +4829,7 @@ var pxt;
4829
4829
  var _a;
4830
4830
  try {
4831
4831
  return typeof window !== "undefined"
4832
- && /^http:\/\/(localhost|127\.0\.0\.1|192\.168\.\d+\.\d+):\d+\//.test(window.location.href)
4832
+ && /^http:\/\/(?:localhost|127\.0\.0\.1|192\.168\.\d{1,3}\.\d{1,3}|[a-zA-Z0-9.-]+\.local):\d+\/?/.test(window.location.href)
4833
4833
  && (ignoreFlags || !/nolocalhost=1/.test(window.location.href))
4834
4834
  && !((_a = pxt === null || pxt === void 0 ? void 0 : pxt.webConfig) === null || _a === void 0 ? void 0 : _a.isStatic);
4835
4835
  }
package/built/pxtsim.d.ts CHANGED
@@ -1059,9 +1059,11 @@ declare namespace pxsim {
1059
1059
  function isPxtElectron(): boolean;
1060
1060
  function isIpcRenderer(): boolean;
1061
1061
  function isElectron(): boolean;
1062
+ function testLocalhost(url: string): boolean;
1062
1063
  function isLocalHost(): boolean;
1063
1064
  function isLocalHostDev(): boolean;
1064
1065
  function unique<T>(arr: T[], f: (t: T) => string): T[];
1066
+ function sanitizeCssName(name: string): string;
1065
1067
  }
1066
1068
  export interface Map<T> {
1067
1069
  [index: string]: T;
@@ -1308,6 +1310,13 @@ declare namespace pxsim {
1308
1310
  aspectRatio?: number;
1309
1311
  permanent?: boolean;
1310
1312
  }>;
1313
+ simulatorExtensions?: pxt.Map<{
1314
+ aspectRatio?: number;
1315
+ permanent?: boolean;
1316
+ index?: string;
1317
+ devUrl?: string;
1318
+ url?: string;
1319
+ }>;
1311
1320
  userLanguage?: string;
1312
1321
  }
1313
1322
  enum SimulatorState {
package/built/pxtsim.js CHANGED
@@ -4712,11 +4712,13 @@ var pxsim;
4712
4712
  return isPxtElectron() || isIpcRenderer();
4713
4713
  }
4714
4714
  U.isElectron = isElectron;
4715
+ function testLocalhost(url) {
4716
+ return /^http:\/\/(?:localhost|127\.0\.0\.1|192\.168\.\d{1,3}\.\d{1,3}|[a-zA-Z0-9.-]+\.local):\d+\/?/.test(url) && !/nolocalhost=1/.test(url);
4717
+ }
4718
+ U.testLocalhost = testLocalhost;
4715
4719
  function isLocalHost() {
4716
4720
  try {
4717
- return typeof window !== "undefined"
4718
- && /^http:\/\/(localhost|127\.0\.0\.1):\d+\//.test(window.location.href)
4719
- && !/nolocalhost=1/.test(window.location.href);
4721
+ return typeof window !== "undefined" && testLocalhost(window.location.href);
4720
4722
  }
4721
4723
  catch (e) {
4722
4724
  return false;
@@ -4740,6 +4742,14 @@ var pxsim;
4740
4742
  return v;
4741
4743
  }
4742
4744
  U.unique = unique;
4745
+ function sanitizeCssName(name) {
4746
+ let sanitized = name.replace(/[^a-zA-Z0-9-_]/g, '_');
4747
+ if (!/^[a-zA-Z_]/.test(sanitized)) {
4748
+ sanitized = 'cls_' + sanitized;
4749
+ }
4750
+ return sanitized;
4751
+ }
4752
+ U.sanitizeCssName = sanitizeCssName;
4743
4753
  })(U = pxsim.U || (pxsim.U = {}));
4744
4754
  class BreakLoopException {
4745
4755
  }
@@ -6116,16 +6126,42 @@ var pxsim;
6116
6126
  this._allowedOrigins.push(options.parentOrigin);
6117
6127
  }
6118
6128
  this._allowedOrigins.push(this.getSimUrl().origin);
6119
- const messageSimulators = options === null || options === void 0 ? void 0 : options.messageSimulators;
6120
- if (messageSimulators) {
6121
- Object.keys(messageSimulators)
6122
- .map(channel => messageSimulators[channel])
6123
- .forEach(messageSimulator => {
6124
- this._allowedOrigins.push(new URL(messageSimulator.url).origin);
6125
- if (messageSimulator.localHostUrl)
6126
- this._allowedOrigins.push(new URL(messageSimulator.localHostUrl).origin);
6127
- });
6128
- }
6129
+ // Legacy support for message simulators
6130
+ const messageSimulators = (options === null || options === void 0 ? void 0 : options.messageSimulators) || {};
6131
+ Object.keys(messageSimulators)
6132
+ .map(channel => messageSimulators[channel])
6133
+ .forEach(messageSimulator => {
6134
+ this._allowedOrigins.push(new URL(messageSimulator.url).origin);
6135
+ if (messageSimulator.localHostUrl)
6136
+ this._allowedOrigins.push(new URL(messageSimulator.localHostUrl).origin);
6137
+ });
6138
+ // Preprocess simulator extensions
6139
+ const simXDevMode = pxsim.U.isLocalHost() && /[?&]simxdev(?:[=&#]|$)/i.test(window.location.href);
6140
+ Object.entries((options === null || options === void 0 ? void 0 : options.simulatorExtensions) || {}).forEach(([key, simx]) => {
6141
+ // Verify essential `simx` config was provided
6142
+ if (!simx ||
6143
+ !simx.index ||
6144
+ !simx.aspectRatio ||
6145
+ simx.permanent === undefined) {
6146
+ return;
6147
+ }
6148
+ // Compute the effective URL
6149
+ if (simXDevMode && simx.devUrl) {
6150
+ // Use the dev URL if the dev flag is set (and we're on localhost)
6151
+ simx.url = new URL(simx.index, simx.devUrl).toString();
6152
+ }
6153
+ else {
6154
+ const simUrl = this.getSimUrl();
6155
+ // Ensure we preserve upload target path (/app/<sha>---simulator)
6156
+ const simPath = simUrl.pathname.replace(/---?.*/, "");
6157
+ // Construct the path. The "-" element delineates the extension key from the resource name.
6158
+ const simxPath = [simPath, "simx", key, "-", simx.index].join("/");
6159
+ // Create the fully-qualified URL, preserving the origin by removing all leading slashes
6160
+ simx.url = new URL(simxPath.replace(/^\/+/, ""), simUrl.origin).toString();
6161
+ }
6162
+ // Add the origin to the allowed origins
6163
+ this._allowedOrigins.push(new URL(simx.url).origin);
6164
+ });
6129
6165
  this._allowedOrigins = pxsim.U.unique(this._allowedOrigins, f => f);
6130
6166
  }
6131
6167
  isDebug() {
@@ -6332,8 +6368,44 @@ var pxsim;
6332
6368
  const messageSimulator = messageChannel &&
6333
6369
  this.options.messageSimulators &&
6334
6370
  this.options.messageSimulators[messageChannel];
6335
- // should we start an extension editor?
6336
- if (messageSimulator) {
6371
+ const simulatorExtension = messageChannel &&
6372
+ this.options.simulatorExtensions &&
6373
+ this.options.simulatorExtensions[messageChannel];
6374
+ const startSimulatorExtension = (url, permanent, aspectRatio) => {
6375
+ var _a;
6376
+ aspectRatio = aspectRatio || ((_a = this._runOptions) === null || _a === void 0 ? void 0 : _a.aspectRatio) || 1.22;
6377
+ let wrapper = this.createFrame(url);
6378
+ this.container.appendChild(wrapper);
6379
+ const messageFrame = wrapper.firstElementChild;
6380
+ messageFrame.dataset[FRAME_DATA_MESSAGE_CHANNEL] = messageChannel;
6381
+ messageFrame.dataset[FRAME_ASPECT_RATIO] = aspectRatio + "";
6382
+ pxsim.U.addClass(wrapper, "simmsg");
6383
+ pxsim.U.addClass(wrapper, "simmsg" + pxsim.U.sanitizeCssName(messageChannel));
6384
+ if (permanent)
6385
+ messageFrame.dataset[PERMANENT] = "true";
6386
+ this.startFrame(messageFrame);
6387
+ frames = this.simFrames(); // refresh
6388
+ };
6389
+ // should we start a simulator extension for this message?
6390
+ if (simulatorExtension) {
6391
+ // find a frame already running that simulator
6392
+ let messageFrame = frames.find(frame => frame.dataset[FRAME_DATA_MESSAGE_CHANNEL] === messageChannel);
6393
+ // not found, spin a new one
6394
+ if (!messageFrame) {
6395
+ const url = new URL(simulatorExtension.url);
6396
+ if (this.options.parentOrigin)
6397
+ url.searchParams.set("parentOrigin", encodeURIComponent(this.options.parentOrigin));
6398
+ if (this.options.userLanguage)
6399
+ url.searchParams.set("language", encodeURIComponent(this.options.userLanguage));
6400
+ startSimulatorExtension(url.toString(), simulatorExtension.permanent, simulatorExtension.aspectRatio);
6401
+ }
6402
+ // not running the current run, restart
6403
+ else if (messageFrame.dataset['runid'] != this.runId) {
6404
+ this.startFrame(messageFrame);
6405
+ }
6406
+ }
6407
+ // (legacy: messageSimulator) should we start a message simulator for this message?
6408
+ else if (messageSimulator) {
6337
6409
  // find a frame already running that simulator
6338
6410
  let messageFrame = frames.find(frame => frame.dataset[FRAME_DATA_MESSAGE_CHANNEL] === messageChannel);
6339
6411
  // not found, spin a new one
@@ -6342,16 +6414,7 @@ var pxsim;
6342
6414
  const url = ((useLocalHost && messageSimulator.localHostUrl) || messageSimulator.url)
6343
6415
  .replace("$PARENT_ORIGIN$", encodeURIComponent(this.options.parentOrigin || ""))
6344
6416
  .replace("$LANGUAGE$", encodeURIComponent(this.options.userLanguage));
6345
- let wrapper = this.createFrame(url);
6346
- this.container.appendChild(wrapper);
6347
- messageFrame = wrapper.firstElementChild;
6348
- messageFrame.dataset[FRAME_DATA_MESSAGE_CHANNEL] = messageChannel;
6349
- pxsim.U.addClass(wrapper, "simmsg");
6350
- pxsim.U.addClass(wrapper, "simmsg" + messageChannel);
6351
- if (messageSimulator.permanent)
6352
- messageFrame.dataset[PERMANENT] = "true";
6353
- this.startFrame(messageFrame);
6354
- frames = this.simFrames(); // refresh
6417
+ startSimulatorExtension(url, messageSimulator.permanent, messageSimulator.aspectRatio);
6355
6418
  }
6356
6419
  // not running the curren run, restart
6357
6420
  else if (messageFrame.dataset['runid'] != this.runId) {
package/built/server.d.ts CHANGED
@@ -12,6 +12,7 @@ export interface ServeOptions {
12
12
  wsPort?: number;
13
13
  serial?: boolean;
14
14
  noauth?: boolean;
15
+ backport?: number;
15
16
  }
16
17
  export declare function compileScriptAsync(id: string): Promise<string>;
17
18
  export declare function serveAsync(options: ServeOptions): Promise<void>;
package/built/server.js CHANGED
@@ -932,6 +932,10 @@ function serveAsync(options) {
932
932
  error(404, "File missing: " + filename);
933
933
  }
934
934
  };
935
+ // Strip /app/hash-sig from URL.
936
+ // This can happen when the locally running backend is serving an uploaded target,
937
+ // but has been configured to route simulator urls to port 3232.
938
+ req.url = req.url.replace(/^\/app\/[0-9a-f]{40}(?:-[0-9a-f]{10})?(.*)$/i, "$1");
935
939
  let uri = url.parse(req.url);
936
940
  let pathname = decodeURI(uri.pathname);
937
941
  const opts = querystring.parse(url.parse(req.url).query);
@@ -1071,6 +1075,26 @@ function serveAsync(options) {
1071
1075
  return error(400, "Invalid asset path");
1072
1076
  }
1073
1077
  }
1078
+ if (elts[0] == "simx" && serveOptions.backport) {
1079
+ // Proxy requests for simulator extensions to the locally running backend.
1080
+ // Should only get here when the backend is running locally and configured to serve the simulator from the cli (via LOCAL_SIM_PORT setting).
1081
+ const passthruOpts = {
1082
+ hostname: uri.hostname,
1083
+ port: serveOptions.backport,
1084
+ path: uri.path,
1085
+ method: req.method,
1086
+ headers: req.headers
1087
+ };
1088
+ const passthruReq = http.request(passthruOpts, passthruRes => {
1089
+ res.writeHead(passthruRes.statusCode, passthruRes.headers);
1090
+ passthruRes.pipe(res);
1091
+ });
1092
+ passthruReq.on("error", e => {
1093
+ console.error(`Error proxying request to port ${serveOptions.backport} .. ${e.message}`);
1094
+ return error(500, e.message);
1095
+ });
1096
+ return req.pipe(passthruReq);
1097
+ }
1074
1098
  if (options.packaged) {
1075
1099
  let filename = path.resolve(path.join(packagedDir, pathname));
1076
1100
  if (nodeutil.fileExistsSync(filename)) {
@@ -1164,6 +1188,19 @@ function serveAsync(options) {
1164
1188
  return;
1165
1189
  }
1166
1190
  }
1191
+ // Look for an .html file corresponding to `/---<pathname>`
1192
+ // Handles serving of `trg-<target>.sim.local:<port>/---simulator`
1193
+ let match = /^\/?---?(.*)/.exec(pathname);
1194
+ if (match && match[1]) {
1195
+ const htmlPathname = `/${match[1]}.html`;
1196
+ for (let dir of dd) {
1197
+ const filename = path.resolve(path.join(dir, htmlPathname));
1198
+ if (nodeutil.fileExistsSync(filename)) {
1199
+ const html = expandHtml(fs.readFileSync(filename, "utf8"), htmlParams);
1200
+ return sendHtml(html);
1201
+ }
1202
+ }
1203
+ }
1167
1204
  if (/simulator\.html/.test(pathname)) {
1168
1205
  // Special handling for missing simulator: redirect to the live sim
1169
1206
  res.writeHead(302, { location: `https://trg-${pxt.appTarget.id}.userpxt.io/---simulator` });