Using OpenAI/Codex to migrate from AU1 to AU2

As mentioned in previous discussions, I’ve tried to use OpenAI and more specific Codex (i.e. OpenAI cli) to migrate Aurelia1 + webpack to Aurelia2 (beta-27) + Vite.

My thoughts are, when the migration documentation is complete, the AI should be smart enough to perform the update. So I fed Codex with the links to the migration documents, and let it run.
Unfortunately I don’t have the original prompt… Codex history does not properly support multi-session it seems… :frowning: But this was kind of what I told Codex to do:

› GOAL: migrate all AU1+webpack code from the /source folder to AU2+vite.
  Step by step actions:
  * Use the documentation with regards to the migration:
  - https://docs.aurelia.io/migrating-to-aurelia-2/migrating-to-aurelia-2
  - https://docs.aurelia.io/migrating-to-aurelia-2/migrating-to-aurelia-2/side-by-side-comparison
  * Use the checklist https://docs.aurelia.io/migrating-to-aurelia-2/migrating-to-aurelia-2/side-by-side-comparison#migration-checklist as base steps
  * all separate steps need to be followed by a commit. 
  * Use Beta-27 as base version to work towards, see https://aurelia.io/blog/2025/12/29/aurelia-beta-27/ for extra info.
  * only summarize output

Runtime about 2h. The commits by Codex give an interesting insight in what it has tried:

chore: add AU1 ui-vite snapshot for AU2 migration POC
chore(au2): update HTML entry point for AU2 root
chore(au2): rewrite main bootstrapping for Aurelia 2
chore(au2): move dependencies to Aurelia 2 beta.27
chore(au2): add Vite bundler setup for AU2
chore(au2): tighten TypeScript config for AU2
chore(au2): switch template requires to imports
chore(au2): drop root template wrappers
chore(au2): align App lifecycle hooks with AU2
chore(au2): rely on convention-based stylesheet loading
chore(au2): modernize custom element/attribute decorators
chore(au2): replace ternary class bindings with .class
chore(au2): remove computedFrom decorators
chore(au2): add lambda expression demo in templates
chore(au2): update value converters and binding behaviors
chore(au2): switch routing config to @route definitions
chore(au2): rename route lifecycle hooks to canLoad/loading
chore(au2): swap router-view for au-viewport
chore(au2): add route param handling smoke check
chore(au2): adapt router guards to AU2 hook API
chore(au2): remove autoinject usage
chore(au2): register services via Registration API
chore(au2): add DI interface token for EntityFactory
chore(au2): switch lazy resolver to AU2 helper
chore(au2): add DI smoke coverage
chore(au2): configure LoggerConfiguration
chore(au2): add logging sink for warn/error
chore(au2): use ILogger for download executor
chore(au2): add logging sink smoke test
chore(au2): add template binding smoke section
chore(au2): add custom element/attribute smoke section
chore(au2): add template controller smoke
chore(au2): add value converter smoke examples
chore(au2): add event binding smoke section
chore(au2): update jest pretest for AU2
chore(au2): refresh component test fixture setup
chore(au2): add lifecycle hook smoke test
chore(au2): update DI usage in unit tests
chore(au2): update e2e smoke for Vite shell
chore(au2): add Shadow DOM demo component
chore(au2): add custom attribute pattern
chore(au2): add minimal state store
chore(au2): add watch decorator smoke

After this first set of changes, Vite was not able to build. I’ve performed some extra changes:

  • There was an issue with @aurelia/i18n at that time, but that might have been temporary
  • Not all PLATFORM.moduleName instances were removed, only those from the router had been changed.
    Probably because in the migration documentation only a router example was added:
    Migrating to Aurelia 2 | The Aurelia 2 Docs

Since still not everything was working, I tried to run again with a clean context and a little bit extra info in the prompt.

› GOAL: migrate all AU1+webpack code from the /source folder to AU2+vite.
  Step by step actions:
  * Read the Aurelia2 documentation, https://docs.aurelia.io/, specifically the documentation with regards to the migration:
  - https://docs.aurelia.io/migrating-to-aurelia-2/migrating-to-aurelia-2
  - https://docs.aurelia.io/migrating-to-aurelia-2/migrating-to-aurelia-2/side-by-side-comparison
  - https://docs.aurelia.io/migrating-to-aurelia-2/cheat-sheet
  * Use the checklist https://docs.aurelia.io/migrating-to-aurelia-2/migrating-to-aurelia-2/side-by-side-comparison#migration-checklist as base steps
  * all separate steps need to be followed by a commit. 
  * Use Beta-27 as base version to work towards, see https://aurelia.io/blog/2025/12/29/aurelia-beta-27/ for extra info.

Unfortunately I forgot to add the summarize output the second run. This caused the available context to get used way faster. Codex has the option to /compact, and does perform that action runtime too.

The resulting summary by Codex of the second run:

• Migrated v1 service usage to AU2/compat and wired compat + dialog config into app startup, while switching dialog opens/settings and task queue usage to AU2 equivalents. Updated bindings to use @aurelia/compat-v1 BindingEngine,
  replaced TaskQueue with queueTask, and moved dialog APIs to @aurelia/dialog with component-based open calls and options-based settings. Added new dependencies and registrations.

  Details and touchpoints:

  - Dialog migration + settings/options: ui-vite/src/_components/dialogs-ux/confirmation/common/cd-confirmation-dialog.ts, ui-vite/src/_components/dialogs-ux/iframe/cd-iframe-dialog.ts, ui-vite/src/_components/dialogs-ux/multi-edit/
    edit-metadata-dialog/bs-edit-metadata.ts, ui-vite/src/core/aurelia/i-dialog-lifecycle.ts, ui-vite/src/core/services/templating/overlay/object-types/overlay-component-interface.ts.
  - Dialog opens now use component: ui-vite/src/core/services/download/link-downloader.ts, ui-vite/src/core/services/actions/action-initiators/select-action-initiator.ts, ui-vite/src/core/services/actions/action-initiators/download-
    action-initiator.ts, ui-vite/src/core/services/actions/action-executors/write-action-executor.ts, ui-vite/src/pages/permalink/permalink-page.ts, ui-vite/src/views/dashboard/widgets/quick-links/url-widget.ts.
  - BindingEngine reverted to compat API and collection observer adjusted for AU2 IndexMap: ui-vite/src/_components/stories/video-studio/_components/video-preview-section.ts (plus many BindingEngine import/update sites).
  - Bootstrapping + deps: ui-vite/src/main.ts, ui-vite/package.json.

With next steps proposed:

  1. Proceed to template migration (.delegate/.call, , etc) per checklist.
  2. Sweep remaining aurelia-framework imports (computedFrom, Binding/View, CompositionEngine, etc).

After this stage I had the prompt available for a moment and ran this as extra command:

For a future run by Codex of this migration, write a new MIGRATION.md file within /ui-vite that contains all relevant information so the new Codex run can start quicker.

The resulting MIGRATIONS.md was very informative and contained a lot of useful information. I will anonymize it and add it to this post for future reference.

The second run my laptop crashed :S so time for the third run:

Read ui-vite/MIGRATION.md and continue with the migration of AU1 to AU2 in the ui-vite folder. Remember to commit on every relevant step, remember to update ui-vite/MIGRATION.md. Hide output if no interaction is needed.

And since the MIGRATION file contains progress as well, Codex was able to immediately continue the migration.


What I noticed from the different runs:

  • PLATFORM was not updated correctly in first run
  • @aurelia/dialog migration was not performed initially
  • since I added the link to the blog post about beta-27, it had more context on what to change because of that.

So this last run ran for another couple of hours.

resulting MIGRATIONS.md

# Aurelia 1 + webpack -> Aurelia 2 + Vite migration notes

This file is a handoff for future Codex runs to continue the migration quickly.

## Context

- Project root: <project root>
- Frontend folder: ui-vite/
- Target version: Aurelia 2 beta.27
- Registry: CodeArtifact (requires `npm run co:login` before installs)
- Network access requires approval in Codex; registry login updates npm config

Docs to keep handy:
- https://docs.aurelia.io/migrating-to-aurelia-2/migrating-to-aurelia-2
- https://docs.aurelia.io/migrating-to-aurelia-2/migrating-to-aurelia-2/side-by-side-comparison
- https://docs.aurelia.io/migrating-to-aurelia-2/cheat-sheet
- Migration checklist: https://docs.aurelia.io/migrating-to-aurelia-2/migrating-to-aurelia-2/side-by-side-comparison#migration-checklist
- Beta.27 blog: https://aurelia.io/blog/2025/12/29/aurelia-beta-27/
  - Note: `queueTask` replaces v1 task queue APIs.

## Commits already done (this branch)

1) 5b121ad4e3 - removed `ui-vite/aurelia_project/`, `ui-vite/webpack*`, `ui-vite/index.ejs`
2) 140ce94fee - removed webpack-only deps/resolutions in `ui-vite/package.json` and updated root devDependencies in lockfile
3) e599684874 - bulk import updates from v1 decorators/i18n to AU2 packages
4) 653d86130a - migrated v1 services/APIs to AU2/compat (TaskQueue -> queueTask, BindingEngine -> compat, Dialog -> @aurelia/dialog, EventAggregator -> IEventAggregator)
5) 3a39c6e5e2 - regenerated `ui-vite/package-lock.json` to include `@aurelia/compat-v1` and `@aurelia/dialog`
6) 26182c8ff2 - updated binding behaviors to AU2 runtime APIs with safe interpolation update handling
7) 320360b308 - removed unused BindingSignaler injection in price select button
8) c69fba163f - replaced v1 BindingEngine usage with AU2 observer wrapper
9) 60b7b7cf8b - renamed lifecycle hooks to AU2 equivalents and removed compat registration/dependency
10) f0fd99a5a2 - regenerated `ui-vite/package-lock.json` after removing `@aurelia/compat-v1`
11) 53c10ef288 - moved binding behavior imports to compat
12) 4aeca8fd0c - moved binding signaler imports to compat
13) 171e72c2bb - moved overlay service imports to compat
14) e5998480dc - updated migration status notes
15) a5b4b915fe - documented `.call` binding strategy
16) efba628091 - migrated routing to `@aurelia/router` (`IRouter` + `router.load`)
17) 26182c8ff2 - updated binding behaviors to AU2 runtime APIs with safe interpolation update handling
18) 320360b308 - removed unused BindingSignaler injection in price select button
19) c69fba163f - replaced v1 BindingEngine usage with AU2 observer wrapper
20) 60b7b7cf8b - renamed lifecycle hooks to AU2 equivalents and removed compat registration/dependency
21) f0fd99a5a2 - regenerated `ui-vite/package-lock.json` after removing `@aurelia/compat-v1`
22) f25e882061 - updated migration notes after lockfile refresh

## Important migration changes already applied

- Added dependencies (pinned):
  - `@aurelia/dialog` in `ui-vite/package.json`.
- App registration includes dialog config:
  - `DialogConfigurationClassic` in `ui-vite/src/main.ts`.
- TaskQueue migrated to `queueTask` in all known TS files.
- Dialog migration:
  - Imports from `@aurelia/dialog`.
  - `dialogService.open({ component: () => import(...) })` (replaced `viewModel`).
  - `dialogController.ok()` is used instead of `close()`.
  - Dialog settings now use `dialogController.settings.options` for `lock`, `overlayDismiss`, `keyboard`.
  - `whenClosed` responses use `response.status === "cancel"`.
- EventAggregator replaced with `IEventAggregator` from `aurelia`.
- Files with `IDisposable` now import from `aurelia`.
- Removed `@computedFrom` usage (AU2 tracks getter dependencies by default).
- `Aurelia` imports in `*_configure.ts` now come from `aurelia`.
- `<require>` template tags now replaced with `<import>`.
- Replaced `.delegate` template bindings with `.trigger` across HTML templates.
- Replaced `bindingMode` with `BindingMode` from `aurelia`.
- Binding behaviors now use AU2 runtime APIs (`SignalBindingBehavior`, `ISignaler`) and handle interpolation update values safely.
- Removed unused compat BindingSignaler injection in price select button.
- Replaced v1 `BindingEngine` usage with an AU2 observer wrapper (`core/services/observation/binding-engine.ts`).
- Renamed v1 lifecycle hooks to AU2 equivalents (`bind` -> `binding`, `unbind` -> `unbinding`, `detached` -> `detaching`).
- Removed `@aurelia/compat-v1` usage and `compatRegistration`.
- Overlay service now uses `IAurelia.enhance` for composition (no v1 composition APIs).
- `.call` template bindings retained for callback bindables (supported by AU2).
- `~sdk` alias now points at `../ui/resources/generated/sdk` (existing SDK output).
- Template imports now use `~/_components` alias (and `~/views` where needed).
- Added `route-path-builder` for `RouteInstructionType` path construction.
- Replaced `aurelia-router` usage with `@aurelia/router` (`IRouter` + `router.load`) and added `route-instruction-resolver` helpers for active route/params.
- `routeConfig` lookups now use `RouteInstructionType` + route id (no more `config.name`); removed `cd-navigation-instruction`.
- Permalink/list/record navigation now listens to `IRouterEvents` instead of observing `currentInstruction`.

## Current state / remaining work

Plan status:
- Step 1 remove webpack/CLI scaffolding: done.
- Step 2 drop webpack deps: done.
- Step 3 update decorators/i18n imports: done.
- Step 4 replace v1 services/APIs: done.
- Step 5 migrate templates (.delegate/.call, <require>, etc): done (keep `.call` for bindable callbacks).
- Step 6 sweep remaining AU1 references/tests/cleanup: pending.

Pending tasks to focus next:
1) Sweep remaining AU1 references/tests/cleanup.

## Helpful commands

- Find remaining AU1 imports:
  - `rg -n "aurelia-framework" ui-vite/src`
  - `rg -n "aurelia-templating-resources" ui-vite/src`
- Find template syntax to change:
  - `rg -n "\.delegate|\.call|<require" ui-vite/src`
- Registry login:
  - `cd ui-vite && npm run co:login`

And now for the main question: did it run?

Well, not completely.

  • We use the webpack proxy functionality, and that was not forwarded to Vite config. But that was not too difficult.
  • @containerless in one of the view-models threw an error:
export function containerless(): (target: Constructable, context: ClassDecoratorContext) => void;
export function containerless(target?: Constructable, context?: ClassDecoratorContext): void | ((target: Constructable, context: ClassDecoratorContext) => void) {
  if (target === void 0) {
    return function ($target: Constructable, $context: ClassDecoratorContext) {
      $context.addInitializer(function (this) {
        markContainerless($target);
      });
    };
  }

  context!.addInitializer(function (this) {
    markContainerless(target);
  });
}


custom-element.ts:184 Uncaught TypeError: Cannot read properties of undefined (reading 'addInitializer')
    at custom-element.ts:184:16

Codex:

I’m considering how esbuild and TypeScript handle decorators in this Aurelia 2 setup; it looks like setting “experimentalDecorators”: false with a target like ES2022 matches Aurelia’s use of standard decorators. The error involving @containerless suggests legacy decorator syntax conflicts, so I’m exploring config changes and alternative component registration methods to align with the new decorator standards and avoid runtime issues.

I recognize this error, but I guess Codex has not encountered this within the migration documents. –> TODO

I’ve pointed Codex to the relevant posts:

Codex has decided to add a Plugin to the vite.config.js file:


const tc39Decorators = (): Plugin => ({
  name: "tc39-decorators",
  enforce: "pre",
  transform(code, id) {
    const filepath = id.split("?", 2)[0];
    if (
      !filepath.endsWith(".ts") ||
      filepath.endsWith(".d.ts") ||
      filepath.includes("/node_modules/")
    ) {
      return;
    }

    const result = ts.transpileModule(code, {
      fileName: filepath,
      compilerOptions: {
        target: ts.ScriptTarget.ES2022,
        module: ts.ModuleKind.ESNext,
        emitDecoratorMetadata: true,
        experimentalDecorators: false,
        useDefineForClassFields: true,
        importHelpers: true,
        sourceMap: true,
      },
    });

    return {
      code: result.outputText,
      map: result.sourceMapText ? JSON.parse(result.sourceMapText) : null,
    };
  },
});

The story continues.. from error to error, and Codex now tries all kinds of Plugin overrulings in vite.config.js.

I don’t think that should be necessary… But I’m also afraid that this experiment comes close to an end… I will place the updated vite.config.js here. If anyone knows what goes wrong, please let me know.

The updated vite.config.ts

import { defineConfig, type Plugin } from "vite";
import aurelia from "@aurelia/vite-plugin";
import { resolve } from "node:path";
import * as ts from "typescript";

const tc39Decorators = (): Plugin => ({
  name: "tc39-decorators",
  enforce: "pre",
  transform(code, id) {
    const filepath = id.split("?", 2)[0];
    if (
      !filepath.endsWith(".ts") ||
      filepath.endsWith(".d.ts") ||
      filepath.includes("/node_modules/")
    ) {
      return;
    }

    const result = ts.transpileModule(code, {
      fileName: filepath,
      transformers: {
        before: [staticInjectTransformer()],
      },
      compilerOptions: {
        target: ts.ScriptTarget.ES2022,
        module: ts.ModuleKind.ESNext,
        emitDecoratorMetadata: true,
        experimentalDecorators: false,
        verbatimModuleSyntax: true,
        useDefineForClassFields: true,
        importHelpers: true,
        sourceMap: true,
      },
    });

    return {
      code: result.outputText,
      map: result.sourceMapText ? JSON.parse(result.sourceMapText) : null,
    };
  },
});

const staticInjectTransformer = (): ts.TransformerFactory<ts.SourceFile> => {
  return (context) => {
    return (sourceFile) => {
      const typeOnlyNames = collectTypeOnlyNames(sourceFile);

      const visit = (node: ts.Node): ts.Node => {
        if (ts.isClassDeclaration(node) && node.name) {
          if (hasStaticInject(node) || hasInjectDecorator(node)) {
            return node;
          }

          const ctor = node.members.find(ts.isConstructorDeclaration);
          if (!ctor || ctor.parameters.length === 0) {
            return node;
          }

          const tokens = ctor.parameters.map((param) =>
            resolveInjectionToken(param.type, typeOnlyNames),
          );

          if (tokens.some((token) => token === null)) {
            return node;
          }

          const injectProperty = ts.factory.createPropertyDeclaration(
            [
              ts.factory.createModifier(ts.SyntaxKind.StaticKeyword),
              ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword),
            ],
            ts.factory.createIdentifier("inject"),
            undefined,
            undefined,
            ts.factory.createArrayLiteralExpression(
              tokens as ts.Expression[],
              false,
            ),
          );

          return ts.factory.updateClassDeclaration(
            node,
            node.modifiers,
            node.name,
            node.typeParameters,
            node.heritageClauses,
            [injectProperty, ...node.members],
          );
        }

        return ts.visitEachChild(node, visit, context);
      };

      const updatedSourceFile = ts.visitNode(sourceFile, visit);
      const valueIdentifiers = collectValueIdentifiers(updatedSourceFile);

      return removeUnusedValueImports(updatedSourceFile, valueIdentifiers, context);
    };
  };
};

const collectTypeOnlyNames = (sourceFile: ts.SourceFile): Set<string> => {
  const names = new Set<string>();

  for (const statement of sourceFile.statements) {
    if (ts.isImportDeclaration(statement)) {
      const clause = statement.importClause;
      if (!clause) {
        continue;
      }

      if (clause.name && clause.isTypeOnly) {
        names.add(clause.name.text);
      }

      const bindings = clause.namedBindings;
      if (bindings && ts.isNamedImports(bindings)) {
        for (const element of bindings.elements) {
          if (clause.isTypeOnly || element.isTypeOnly) {
            names.add(element.name.text);
          }
        }
      }
    } else if (
      ts.isInterfaceDeclaration(statement) ||
      ts.isTypeAliasDeclaration(statement)
    ) {
      names.add(statement.name.text);
    }
  }

  return names;
};

const collectValueIdentifiers = (sourceFile: ts.SourceFile): Set<string> => {
  const identifiers = new Set<string>();

  const visit = (node: ts.Node, inTypePosition: boolean) => {
    if (
      ts.isTypeNode(node) ||
      ts.isTypeElement(node) ||
      ts.isTypeParameterDeclaration(node) ||
      ts.isInterfaceDeclaration(node) ||
      ts.isTypeAliasDeclaration(node)
    ) {
      ts.forEachChild(node, (child) => visit(child, true));
      return;
    }

    if (ts.isHeritageClause(node)) {
      const isImplements = node.token === ts.SyntaxKind.ImplementsKeyword;
      for (const type of node.types) {
        visit(type, inTypePosition || isImplements);
      }
      return;
    }

    if (ts.isExpressionWithTypeArguments(node)) {
      visit(node.expression, inTypePosition);
      if (node.typeArguments) {
        for (const typeArgument of node.typeArguments) {
          visit(typeArgument, true);
        }
      }
      return;
    }

    if (ts.isTypeQueryNode(node)) {
      visit(node.exprName, true);
      return;
    }

    if (!inTypePosition && ts.isIdentifier(node) && !isDeclarationIdentifier(node)) {
      identifiers.add(node.text);
    }

    ts.forEachChild(node, (child) => visit(child, inTypePosition));
  };

  visit(sourceFile, false);

  return identifiers;
};

const isDeclarationIdentifier = (node: ts.Identifier): boolean => {
  const parent = node.parent;
  if (!parent) {
    return false;
  }

  if (
    ts.isImportClause(parent) ||
    ts.isImportSpecifier(parent) ||
    ts.isNamespaceImport(parent) ||
    ts.isImportEqualsDeclaration(parent) ||
    ts.isExportSpecifier(parent) ||
    ts.isNamespaceExportDeclaration(parent)
  ) {
    return true;
  }

  if (
    (ts.isClassDeclaration(parent) ||
      ts.isFunctionDeclaration(parent) ||
      ts.isInterfaceDeclaration(parent) ||
      ts.isTypeAliasDeclaration(parent) ||
      ts.isEnumDeclaration(parent) ||
      ts.isModuleDeclaration(parent) ||
      ts.isVariableDeclaration(parent) ||
      ts.isParameter(parent) ||
      ts.isPropertyDeclaration(parent) ||
      ts.isPropertySignature(parent) ||
      ts.isMethodDeclaration(parent) ||
      ts.isMethodSignature(parent) ||
      ts.isGetAccessorDeclaration(parent) ||
      ts.isSetAccessorDeclaration(parent)) &&
    parent.name === node
  ) {
    return true;
  }

  if (ts.isBindingElement(parent) && parent.name === node) {
    return true;
  }

  if (ts.isPropertyAssignment(parent) && parent.name === node) {
    return true;
  }

  if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
    return true;
  }

  if (ts.isQualifiedName(parent) && parent.right === node) {
    return true;
  }

  return false;
};

const removeUnusedValueImports = (
  sourceFile: ts.SourceFile,
  valueIdentifiers: Set<string>,
  context: ts.TransformationContext,
): ts.SourceFile => {
  const visit = (node: ts.Node): ts.Node | undefined => {
    if (ts.isImportDeclaration(node) && node.importClause) {
      if (node.importClause.isTypeOnly) {
        return node;
      }

      const clause = node.importClause;
      const defaultName = clause.name;
      const keepDefault = defaultName && valueIdentifiers.has(defaultName.text);
      let namedBindings = clause.namedBindings;

      if (namedBindings && ts.isNamedImports(namedBindings)) {
        const keptBindings = namedBindings.elements.filter((element) =>
          valueIdentifiers.has(element.name.text),
        );
        namedBindings = keptBindings.length
          ? ts.factory.updateNamedImports(namedBindings, keptBindings)
          : undefined;
      } else if (namedBindings && ts.isNamespaceImport(namedBindings)) {
        namedBindings = valueIdentifiers.has(namedBindings.name.text)
          ? namedBindings
          : undefined;
      }

      if (!keepDefault && !namedBindings) {
        return undefined;
      }

      const updatedClause = ts.factory.updateImportClause(
        clause,
        clause.isTypeOnly,
        keepDefault ? defaultName : undefined,
        namedBindings,
      );

      return ts.factory.updateImportDeclaration(
        node,
        node.modifiers,
        updatedClause,
        node.moduleSpecifier,
        node.assertClause,
      );
    }

    return ts.visitEachChild(node, visit, context);
  };

  return ts.visitNode(sourceFile, visit);
};

const hasStaticInject = (node: ts.ClassDeclaration): boolean => {
  return node.members.some((member) => {
    if (!ts.isPropertyDeclaration(member) || !member.name) {
      return false;
    }

    if (!member.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword)) {
      return false;
    }

    return (
      ts.isIdentifier(member.name) && member.name.text === "inject"
    );
  });
};

const hasInjectDecorator = (node: ts.ClassDeclaration): boolean => {
  const decorators = ts.canHaveDecorators(node) ? ts.getDecorators(node) : undefined;
  if (!decorators) {
    return false;
  }

  return decorators.some((decorator) => {
    const expr = decorator.expression;
    if (ts.isIdentifier(expr)) {
      return expr.text === "inject";
    }
    if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression)) {
      return expr.expression.text === "inject";
    }
    return false;
  });
};

const resolveInjectionToken = (
  typeNode: ts.TypeNode | undefined,
  typeOnlyNames: Set<string>,
): ts.Expression | null => {
  if (!typeNode) {
    return null;
  }

  if (ts.isParenthesizedTypeNode(typeNode)) {
    return resolveInjectionToken(typeNode.type, typeOnlyNames);
  }

  if (ts.isUnionTypeNode(typeNode) || ts.isIntersectionTypeNode(typeNode)) {
    for (const type of typeNode.types) {
      const resolved = resolveInjectionToken(type, typeOnlyNames);
      if (resolved) {
        return resolved;
      }
    }
    return null;
  }

  if (ts.isTypeReferenceNode(typeNode)) {
    return resolveTypeName(typeNode.typeName, typeOnlyNames);
  }

  if (ts.isTypeQueryNode(typeNode)) {
    return resolveEntityName(typeNode.exprName, typeOnlyNames);
  }

  if (ts.isTypeOperatorNode(typeNode)) {
    return resolveInjectionToken(typeNode.type, typeOnlyNames);
  }

  return null;
};

const resolveTypeName = (
  typeName: ts.EntityName,
  typeOnlyNames: Set<string>,
): ts.Expression | null => {
  if (ts.isIdentifier(typeName)) {
    if (typeOnlyNames.has(typeName.text)) {
      return null;
    }
    if (excludedTypeNames.has(typeName.text)) {
      return null;
    }
    return ts.factory.createIdentifier(typeName.text);
  }

  const leftmost = getLeftmostIdentifier(typeName);
  if (leftmost && typeOnlyNames.has(leftmost.text)) {
    return null;
  }

  const left = resolveTypeName(typeName.left, typeOnlyNames);
  if (!left) {
    return null;
  }

  return ts.factory.createPropertyAccessExpression(left, typeName.right);
};

const resolveEntityName = (
  entityName: ts.EntityName,
  typeOnlyNames: Set<string>,
): ts.Expression | null => {
  if (ts.isIdentifier(entityName)) {
    if (typeOnlyNames.has(entityName.text)) {
      return null;
    }
    if (excludedTypeNames.has(entityName.text)) {
      return null;
    }
    return ts.factory.createIdentifier(entityName.text);
  }

  const leftmost = getLeftmostIdentifier(entityName);
  if (leftmost && typeOnlyNames.has(leftmost.text)) {
    return null;
  }

  const left = resolveEntityName(entityName.left, typeOnlyNames);
  if (!left) {
    return null;
  }

  return ts.factory.createPropertyAccessExpression(left, entityName.right);
};

const getLeftmostIdentifier = (entity: ts.EntityName): ts.Identifier | null => {
  if (ts.isIdentifier(entity)) {
    return entity;
  }

  return getLeftmostIdentifier(entity.left);
};

const excludedTypeNames = new Set([
  "Array",
  "Boolean",
  "Date",
  "Function",
  "Map",
  "Number",
  "Object",
  "Promise",
  "RegExp",
  "Set",
  "String",
]);

export default defineConfig({
  plugins: [aurelia(), tc39Decorators()],
  publicDir: "static",
  resolve: {
    alias: {
      "~": resolve(__dirname, "src"),
      "~sdk": resolve(__dirname, "../ui/resources/generated/sdk"),
    },
  },
  esbuild: {
    target: "es2022",
  },
  build: {
    outDir: "www",
    target: "es2022",
  },
  optimizeDeps: {
    esbuildOptions: {
      target: "es2022",
    },
  },
  server: {
    proxy: {
      // string shorthand:
      // http://localhost:5173/foo
      //   -> http://localhost:4567/foo
      '/app': {
        target: 'http://ng-local',
        changeOrigin:false,
      },
      '/admin': 'http://ng-local',
      '/xldoc': 'http://ng-local',
      '/brandspot': 'http://ng-local',
      // with options:
      // http://localhost:5173/api/bar
      //   -> http://jsonplaceholder.typicode.com/bar
      // '/api': {
      //   target: 'http://jsonplaceholder.typicode.com',
      //   changeOrigin: true,
      //   rewrite: (path) => path.replace(/^\/api/, ''),
      // },
      // // with RegExp:
      // // http://localhost:5173/fallback/
      // //   -> http://jsonplaceholder.typicode.com/
      // '^/fallback/.*': {
      //   target: 'http://jsonplaceholder.typicode.com',
      //   changeOrigin: true,
      //   rewrite: (path) => path.replace(/^\/fallback/, ''),
      // },
      // // Using the proxy instance
      // '/api': {
      //   target: 'http://jsonplaceholder.typicode.com',
      //   changeOrigin: true,
      //   configure: (proxy, options) => {
      //     // proxy will be an instance of 'http-proxy'
      //   },
      // },
      // // Proxying websockets or socket.io:
      // // ws://localhost:5173/socket.io
      // //   -> ws://localhost:5174/socket.io
      // // Exercise caution using `rewriteWsOrigin` as it can leave the
      // // proxying open to CSRF attacks.
      // '/socket.io': {
      //   target: 'ws://localhost:5174',
      //   ws: true,
      //   rewriteWsOrigin: true,
      // },
    },
  },
});

Well, one extra note. Next errors don’t seem to be directly related to AU/vite, but more related to how we had things set up in AU1. Little steps…

After a couple of days copy/pasting errors from the browser console into Codex, let’s get to a conclusion.

  1. The application does start up. So some buildable system can be reached by letting Codex do the migration
  2. however, a lot of functionality is still incorrect. I think that’s mostly because of Codex interpretation going haywire, or simply assuming the incorrect things because the boundaries are not clear enough. Bounderies in this case can be both the migration rules and the general AU2 documentation, not sure.

Even though I’m a bit disappointed with the end result of this experiment, I do still think a better result can be reached.

I had Codex use the migration checklist as base for the migration actions. I think when the necessary steps are documented a bit more, with more specific information per step, Codex (or any other AI) can be guided better.

Also, a lot of effort went into converting the old code to match the TC39 decorators. That is probably due to a missing and/or incorrect clean project. Specifically the tsconfig and vite.config I think need some more information.

Not now, but maybe in future I will retry using a more strict starting prompt.

So again, not there yet, but hopeful for the (near) future! I hope this exercise gives inspiration to others who want to try the migration themselves. Good luck and please share your experiences!

5 Likes

Meant to reply the other day, but this is insanely useful stuff. As a lot of devs are using AI assist now, I think ensuring the migration docs are AI friendly is important and looking through all of this, I can see a few improvements we can make to our docs to make AI struggle less and things a bit more obvious to an AI reasoning. I pretty much started working on refined docs, I’ll probably PR them tomorrow hopefully.

4 Likes

@mroeling I’ve tried your prompt on a smaller chunk of just 2 views and a dialog. It went good.
Didn’t even have to clean the result.

Mind you, I didn’t start from scratch - already had a buildable app skeleton with a few views and told Codex to use those as an example.

1 Like