Skip to content

ILST: Fix/private constructor dom classes#160

Open
MBecherKurz wants to merge 1 commit into
docsforadobe:masterfrom
MBecherKurz:fix/private-constructor-dom-classes
Open

ILST: Fix/private constructor dom classes#160
MBecherKurz wants to merge 1 commit into
docsforadobe:masterfrom
MBecherKurz:fix/private-constructor-dom-classes

Conversation

@MBecherKurz
Copy link
Copy Markdown

Many Illustrator DOM classes cannot be instantiated from user code — e.g. new Layer(), new Document(), new PageItems() all throw at runtime ("does not have a constructor" or the class has no global). These objects are handed to the script by the host: app.documents, doc.layers, app.preferences, doc.pageItems[0], etc. New instances can only be created through other Illustrator methods like app.activeDocument.layers.add() etc.

The TypeScript typedef previously allowed new X() for every class, which caught no misuse. private constructor() makes these misuses a compile error while still letting the class name be used as a type.

Base classes Color and PageItem are intentionally left with a public constructor so their existing subclasses (LabColor, PathItem, …) can still extend them.

Note this was tested in the same ways as my other PR-159 with a short test script executed in Illustrator 2026 and 2025. But I'm quite sure that was already the case in older Illustrator versions.

The jsx script used to identify this.
/*
 * Illustrator ExtendScript reflect probe.
 *
 * For every Illustrator scripting class this script answers two questions:
 *
 *   1. Can user code `new ClassName()`?
 *        → try constructing. If the engine rejects the call (no global, or
 *          "does not have a constructor"), the TypeScript typedef must mark
 *          the class with `private constructor()` so `new X()` is a type
 *          error. Otherwise the default public constructor is correct.
 *
 *   2. What is the real public API of an instance?
 *        → walk `instance.reflect.properties` and `instance.reflect.methods`.
 *          Everything that surfaces here is an instance (a.k.a. public)
 *          member. This is the authoritative list for the typedef body.
 *
 * Note on "static": ExtendScript's class dictionaries mirror the instance
 * schema onto the constructor (reading `Paper.paperInfo` returns a metadata
 * handle, and writing to it persists as a plain Function-object property).
 * That is not a real static member — it is a quirk of the engine's
 * introspection. Only instance reflection is reported below, so the report
 * lines up 1:1 with how the API is actually used and typed.
 *
 * Usage:
 *   1. Open Illustrator 2022 (or newer).
 *   2. Open at least one document (classes like Layer, Swatch, TextFrame
 *      only exist inside a document).
 *   3. File > Scripts > Other Script… > select this file.
 *   4. Report is written to ~/Desktop/ai-reflect-report.txt.
 */

#target illustrator

(function () {
  var out = [];
  function log(s) { out.push(s); }

  // Members every ExtendScript object carries; not part of the class's API.
  var BUILTINS = { __proto__: 1, reflect: 1 };

  function listMembers(members) {
    var a = [];
    for (var i = 0; i < members.length; i++) {
      var name = members[i].name;
      if (BUILTINS[name]) continue;
      a.push(name);
    }
    a.sort();
    return a;
  }

  // Try to build an instance. Returns {ok, instance, reason}.
  //   ok=true  → `new X()` works, keep default public constructor in typedef.
  //   ok=false → typedef needs `private constructor()`.
  function tryConstruct(name) {
    var ctor;
    try { ctor = $.global[name]; } catch (e) {
      return { ok: false, reason: "no global '" + name + "'" };
    }
    if (typeof ctor === "undefined" || ctor === null) {
      return { ok: false, reason: "no global '" + name + "'" };
    }
    try {
      var inst = new ctor();
      return { ok: true, instance: inst };
    } catch (e) {
      return { ok: false, reason: "new " + name + "() threw: " + e };
    }
  }

  function probe(entry) {
    log("================ " + entry.name + " ================");

    // 1. Constructor check — drives `private constructor()` decision.
    var ctorCheck = tryConstruct(entry.name);
    if (ctorCheck.ok) {
      log("  constructor: public  (new " + entry.name + "() succeeded)");
    } else {
      log("  constructor: private (" + ctorCheck.reason + ")");
    }

    // 2. Instance API — drives the typedef body.
    var inst = ctorCheck.ok ? ctorCheck.instance : null;
    if (!inst) {
      try { inst = entry.sample(); } catch (e) {
        log("  (no live instance: " + e + ")");
        log("");
        return;
      }
    }
    if (!inst) {
      log("  (no live instance available" + (entry.note ? " — " + entry.note : "") + ")");
      log("");
      return;
    }

    try {
      var props   = listMembers(inst.reflect.properties);
      var methods = listMembers(inst.reflect.methods);
      log("  props:   " + (props.length   ? props.join(", ")   : "—"));
      log("  methods: " + (methods.length ? methods.join(", ") : "—"));
    } catch (e) {
      log("  reflect failed on instance: " + e);
    }
    log("");
  }

  // Live-instance sources for classes that can't (or shouldn't) be `new`'d.
  // The constructor check still runs first; `sample` is only consulted when
  // construction fails or returns nothing.
  var classes = [
    // Instantiable value/options classes — `new` works; sample is the fallback.
    { name: "Matrix",                       sample: function () { return new Matrix(); } },
    { name: "RGBColor",                     sample: function () { return new RGBColor(); } },
    { name: "CMYKColor",                    sample: function () { return new CMYKColor(); } },
    { name: "GrayColor",                    sample: function () { return new GrayColor(); } },
    { name: "LabColor",                     sample: function () { return new LabColor(); } },
    { name: "NoColor",                      sample: function () { return new NoColor(); } },
    { name: "SpotColor",                    sample: function () { return new SpotColor(); } },
    { name: "PatternColor",                 sample: function () { return new PatternColor(); } },
    { name: "GradientColor",                sample: function () { return new GradientColor(); } },
    { name: "ExportOptionsJPEG",            sample: function () { return new ExportOptionsJPEG(); } },
    { name: "ExportOptionsPNG8",            sample: function () { return new ExportOptionsPNG8(); } },
    { name: "ExportOptionsPNG24",           sample: function () { return new ExportOptionsPNG24(); } },
    { name: "ExportOptionsGIF",             sample: function () { return new ExportOptionsGIF(); } },
    { name: "ExportOptionsPhotoshop",       sample: function () { return new ExportOptionsPhotoshop(); } },
    { name: "ExportOptionsSVG",             sample: function () { return new ExportOptionsSVG(); } },
    { name: "ExportOptionsTIFF",            sample: function () { return new ExportOptionsTIFF(); } },
    { name: "ExportOptionsAutoCAD",         sample: function () { return new ExportOptionsAutoCAD(); } },
    { name: "ExportOptionsWebOptimizedSVG", sample: function () { return new ExportOptionsWebOptimizedSVG(); } },
    { name: "OpenOptions",                  sample: function () { return new OpenOptions(); } },
    { name: "FXGSaveOptions",               sample: function () { return new FXGSaveOptions(); } },
    { name: "EPSSaveOptions",               sample: function () { return new EPSSaveOptions(); } },
    { name: "PDFSaveOptions",               sample: function () { return new PDFSaveOptions(); } },
    { name: "IllustratorSaveOptions",       sample: function () { return new IllustratorSaveOptions(); } },
    { name: "PrintOptions",                 sample: function () { return new PrintOptions(); } },
    { name: "PrintPaperOptions",            sample: function () { return new PrintPaperOptions(); } },
    { name: "PrintJobOptions",              sample: function () { return new PrintJobOptions(); } },
    { name: "PrintColorSeparationOptions",  sample: function () { return new PrintColorSeparationOptions(); } },
    { name: "PrintCoordinateOptions",       sample: function () { return new PrintCoordinateOptions(); } },
    { name: "PrintPageMarksOptions",        sample: function () { return new PrintPageMarksOptions(); } },
    { name: "PrintFontOptions",             sample: function () { return new PrintFontOptions(); } },
    { name: "PrintPostScriptOptions",       sample: function () { return new PrintPostScriptOptions(); } },
    { name: "PrintColorManagementOptions",  sample: function () { return new PrintColorManagementOptions(); } },
    { name: "PrintFlattenerOptions",        sample: function () { return new PrintFlattenerOptions(); } },
    { name: "DocumentPreset",               sample: function () { return new DocumentPreset(); } },
    { name: "ImageCaptureOptions",          sample: function () { return new ImageCaptureOptions(); } },
    { name: "RasterEffectOptions",          sample: function () { return new RasterEffectOptions(); } },
    { name: "RasterizeOptions",             sample: function () { return new RasterizeOptions(); } },
    { name: "TabStopInfo",                  sample: function () { return new TabStopInfo(); } },

    // Classes docs say are `new`-able but the engine refuses — probed via
    // sample-only paths.
    { name: "ExportOptionsFlash",           sample: function () { return null; } },
    { name: "OpenOptionsPhotoshop",         sample: function () { return app.preferences.photoshopFileOptions; } },
    { name: "OpenOptionsPDF",               sample: function () { return app.preferences.PDFFileOptions; } },
    { name: "OpenOptionsAutoCAD",           sample: function () { return app.preferences.AutoCADFileOptions; } },
    { name: "TracingOptions",               sample: function () { return null; } },

    // Singletons / DOM roots.
    { name: "Application",                  sample: function () { return app; } },
    { name: "Preferences",                  sample: function () { return app.preferences; } },

    // Printer family — only available when a printer is configured.
    { name: "Printer", sample: function () {
        var l = (typeof app.getPrinterList === "function") ? app.getPrinterList() : null;
        return (l && l.length) ? l[0] : null;
      }, note: "no printer configured" },
    { name: "PrinterInfo", sample: function () {
        var l = (typeof app.getPrinterList === "function") ? app.getPrinterList() : null;
        return (l && l.length) ? l[0].printerInfo : null;
      }, note: "no printer configured" },
    { name: "Paper", sample: function () {
        var l = (typeof app.getPrinterList === "function") ? app.getPrinterList() : null;
        var papers = l && l.length && l[0].printerInfo && l[0].printerInfo.paperList;
        return (papers && papers.length) ? papers[0] : null;
      }, note: "no printer configured" },
    { name: "PaperInfo", sample: function () {
        var l = (typeof app.getPrinterList === "function") ? app.getPrinterList() : null;
        var papers = l && l.length && l[0].printerInfo && l[0].printerInfo.paperList;
        return (papers && papers.length) ? papers[0].paperInfo : null;
      }, note: "no printer configured" },
    { name: "Ink", sample: function () {
        var l = (typeof app.getPrinterList === "function") ? app.getPrinterList() : null;
        var inks = l && l.length && l[0].printerInfo && l[0].printerInfo.inkList;
        return (inks && inks.length) ? inks[0] : null;
      }, note: "no printer configured" },
    { name: "InkInfo", sample: function () {
        var l = (typeof app.getPrinterList === "function") ? app.getPrinterList() : null;
        var inks = l && l.length && l[0].printerInfo && l[0].printerInfo.inkList;
        return (inks && inks.length) ? inks[0].inkInfo : null;
      }, note: "no printer configured" }
  ];

  // Document-scoped classes: only probable when a document is open.
  if (app.documents && app.documents.length > 0) {
    var doc = app.activeDocument;
    var first = function (coll) { return (coll && coll.length) ? coll[0] : null; };

    classes.push({ name: "Document",         sample: function () { return doc; } });
    classes.push({ name: "Documents",        sample: function () { return app.documents; } });
    classes.push({ name: "Layer",            sample: function () { return first(doc.layers); } });
    classes.push({ name: "Layers",           sample: function () { return doc.layers; } });
    classes.push({ name: "Artboard",         sample: function () { return first(doc.artboards); } });
    classes.push({ name: "Artboards",        sample: function () { return doc.artboards; } });
    classes.push({ name: "View",             sample: function () { return first(doc.views); } });
    classes.push({ name: "Views",            sample: function () { return doc.views; } });
    classes.push({ name: "Swatch",           sample: function () { return first(doc.swatches); } });
    classes.push({ name: "SwatchGroup",      sample: function () { return first(doc.swatchGroups); } });
    classes.push({ name: "Spot",             sample: function () { return first(doc.spots); } });
    classes.push({ name: "Gradient",         sample: function () { return first(doc.gradients); } });
    classes.push({ name: "GradientStop",     sample: function () {
      return first(doc.gradients) ? first(first(doc.gradients).gradientStops) : null;
    } });
    classes.push({ name: "Pattern",          sample: function () { return first(doc.patterns); } });
    classes.push({ name: "Symbol",           sample: function () { return first(doc.symbols); } });
    classes.push({ name: "Brush",            sample: function () { return first(doc.brushes); } });
    classes.push({ name: "ArtStyle",         sample: function () { return first(doc.artStyles); } });
    classes.push({ name: "TextFont",         sample: function () { return first(app.textFonts); } });
    classes.push({ name: "PageItem",         sample: function () { return first(doc.pageItems); } });
    classes.push({ name: "PathItem",         sample: function () { return first(doc.pathItems); } });
    classes.push({ name: "PathPoint",        sample: function () {
      return first(doc.pathItems) ? first(first(doc.pathItems).pathPoints) : null;
    } });
    classes.push({ name: "CompoundPathItem", sample: function () { return first(doc.compoundPathItems); } });
    classes.push({ name: "GroupItem",        sample: function () { return first(doc.groupItems); } });
    classes.push({ name: "CharacterStyle",   sample: function () { return first(doc.characterStyles); } });
    classes.push({ name: "ParagraphStyle",   sample: function () { return first(doc.paragraphStyles); } });
  }

  log("Illustrator reflect probe");
  log("app:        " + app.name + " " + app.version);
  log("generated:  " + new Date());
  log("");
  log("Decisions per class:");
  log("  constructor: public   → keep default TS constructor");
  log("  constructor: private  → typedef needs `private constructor()`");
  log("  props / methods       → instance API surface (goes in class body)");
  log("");

  for (var i = 0; i < classes.length; i++) probe(classes[i]);

  var f = new File(Folder.desktop + "/ai-reflect-report.txt");
  f.encoding = "UTF-8";
  f.open("w");
  f.write(out.join("\n"));
  f.close();

  alert("Probe complete.\n\n" + classes.length + " classes.\nReport: " + f.fsName);
})();

Many Illustrator DOM classes cannot be instantiated from user code —
e.g. `new Layer()`, `new Document()`, `new PageItems()` all throw at
runtime ("does not have a constructor" or the class has no global).
These objects are handed to the script by the host: `app.documents`,
`doc.layers`, `app.preferences`, `doc.pageItems[0]`, etc.

The TypeScript typedef previously allowed `new X()` for every class,
which caught no misuse. `private constructor()` makes these misuses a
compile error while still letting the class name be used as a type.

Base classes Color and PageItem are intentionally left with a public
constructor so their existing subclasses (LabColor, PathItem, …) can
still extend them.
@MBecherKurz MBecherKurz changed the title Fix/private constructor dom classes ILST: Fix/private constructor dom classes Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant