Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
42b54f2
JavaScript: locate an AMD block by node kind
knutwannheden Aug 27, 2026
0e68c3e
JavaScript: cover namespaced-callee matching and literal empty AMD ar…
knutwannheden Aug 27, 2026
6397d17
JavaScript: read module bindings from either an AMD block or ES imports
knutwannheden Aug 27, 2026
ae09ac9
JavaScript: cover moduleBindings' AMD lane, member exclusion, and com…
knutwannheden Aug 27, 2026
178e343
JavaScript: add and remove AMD dependencies index-aligned with the fa…
knutwannheden Aug 27, 2026
8808d8c
ADR 0013: the read side makes hoisting recipes lane-agnostic
knutwannheden Aug 27, 2026
abfe510
JavaScript: normalize arrow parameter lists before editing them
knutwannheden Aug 27, 2026
f8e076c
JavaScript: bindModule gives a recipe a local binding on either lane
knutwannheden Aug 27, 2026
e3d297c
JavaScript: bindModule's AMD gate skips a binding nothing goes on to …
knutwannheden Aug 27, 2026
fb92e29
JavaScript: bindModule dedupes AMD reservations and never deletes the…
knutwannheden Aug 27, 2026
5fc933f
JavaScript: maybeRemoveImport drops an unused AMD dependency
knutwannheden Aug 27, 2026
e06bf84
JavaScript: bindModule/withDependency refuse on any parameter/depende…
knutwannheden Aug 27, 2026
4491950
JavaScript: cover the expression-bodied AMD arrow factory the count-m…
knutwannheden Aug 27, 2026
cd6b713
JavaScript: drop AMD bindings a rewrite left unreferenced
knutwannheden Aug 27, 2026
ab3b059
JavaScript: single-walk namesUsed for removeNewlyUnusedBindings, plus…
knutwannheden Aug 27, 2026
bdc531b
JavaScript: fix AMD binding-module correctness gaps from final review
knutwannheden Aug 27, 2026
da75865
JavaScript: cover nested destructuring in the AMD declaredNames fix
knutwannheden Aug 27, 2026
c61a9dd
JavaScript: handle rest elements in declaredNames; trim removeEntry's…
knutwannheden Aug 27, 2026
efdc4e2
JavaScript: moduleSystem reports "none" for a plain script
knutwannheden Aug 27, 2026
fbcc726
JavaScript: trim two bindModule comments to what their neighbours do …
knutwannheden Aug 27, 2026
978024c
JavaScript: bindModule deconflicts through the scope utility, and is …
knutwannheden Aug 27, 2026
a7dbd1a
JavaScript: unify maybeAddImport and bindModule into maybeBind
knutwannheden Aug 27, 2026
423c48e
JavaScript: add maybeUnbind, mirroring maybeBind on the removal side
knutwannheden Aug 27, 2026
9a71d10
JavaScript: refuse CommonJS creation symmetrically, fix .mjs/.mts det…
knutwannheden Aug 28, 2026
79675bb
JavaScript: distinguish default and namespace binding shapes in maybe…
knutwannheden Aug 28, 2026
3f0bac9
JavaScript: recognise dynamic import and top-level await in maybeBind
knutwannheden Aug 28, 2026
d179c9f
JavaScript: three small fixes from an earlier binding review
knutwannheden Aug 28, 2026
2faf6de
JavaScript: moduleOf carries the same shape caveat bindingOf does
knutwannheden Aug 28, 2026
484c37c
JavaScript: name the caller that wants a namespace binding
knutwannheden Aug 28, 2026
1ebd8ba
JavaScript: add maybeRebind, the third binding primitive
knutwannheden Aug 28, 2026
13791e2
JavaScript: accept a bare module string on maybeBind and maybeUnbind
knutwannheden Aug 28, 2026
65b26e3
JavaScript: remove ChangeImport's newAlias option; document RemoveImp…
knutwannheden Aug 28, 2026
eba5da4
JavaScript: fix ten verified defects in maybeBind/maybeRebind found b…
knutwannheden Aug 28, 2026
5d52f60
JavaScript: fix two more defects from a review of the stacked branch
knutwannheden Aug 28, 2026
4cdcc43
JavaScript: close two residuals from an independent verification pass
knutwannheden Aug 28, 2026
b124388
JavaScript: share the module-binding helpers through scope.ts
knutwannheden Aug 28, 2026
80fcc20
JavaScript: Template.resolveBindings calls maybeBind
knutwannheden Aug 28, 2026
cfc7aa8
JavaScript: a template's context statements declare its module bindings
knutwannheden Aug 28, 2026
402cd6d
JavaScript: drop the AddImportOptions import ModuleBinding left behind
knutwannheden Aug 28, 2026
03f5e2c
JavaScript: a rule refuses to apply a template whose bindings it cann…
knutwannheden Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
408 changes: 323 additions & 85 deletions rewrite-javascript/rewrite/src/javascript/add-import.ts

Large diffs are not rendered by default.

902 changes: 902 additions & 0 deletions rewrite-javascript/rewrite/src/javascript/amd.ts

Large diffs are not rendered by default.

435 changes: 435 additions & 0 deletions rewrite-javascript/rewrite/src/javascript/binding.ts

Large diffs are not rendered by default.

14 changes: 12 additions & 2 deletions rewrite-javascript/rewrite/src/javascript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,18 @@ export * from "./autodetect";
export * from "./tree-debug";
export * from "./project-parser";

export * from "./scope";
export * from "./add-import";
export type {Scope} from "./scope";
export {scopeOf, namesDeclaredIn, bindingNames} from "./scope";
export type {QuoteChar, AddImportOptions} from "./add-import";
export {ImportStyle, moduleNameOf, AddImport} from "./add-import";
// AMD mechanics `recipes-ui5` builds on directly, beyond the `maybeBind` surface below.
export type {AmdBlock} from "./amd";
export {
DEFAULT_AMD_CALLEES, amdBlockOf, dependencyNames, parameterNames, withDependency, withoutDependencyAt,
RemoveAmdDependency, removeNewlyUnusedAmdBindings
} from "./amd";
export type {MaybeBindOptions, MaybeUnbindOptions, MaybeRebindOptions, ModuleBindings} from "./binding";
export {maybeBind, maybeUnbind, maybeRebind, moduleBindings, isAmdBlock, maybeRemoveImport, maybeAddImport} from "./binding";
export * from "./remove-import";
export * from "./cleanup/index";
export * from "./recipes/index";
Expand Down
10 changes: 9 additions & 1 deletion rewrite-javascript/rewrite/src/javascript/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ import SpreadAttribute = JSX.SpreadAttribute;
export interface JavaScriptParserOptions extends ParserOptions {
styles?: NamedStyles[],
sourceFileCache?: Map<string, ts.SourceFile>,
/**
* Type packages to load whose declarations nothing imports. Unset leaves TypeScript's default,
* which reads `@types/*` and nothing else; a package declaring its modules ambiently needs
* naming here for those declarations to be in scope.
*/
types?: string[],
}

function getScriptKindFromFileName(fileName: string): ts.ScriptKind {
Expand Down Expand Up @@ -86,6 +92,7 @@ export class JavaScriptParser extends Parser {
relativeTo,
styles,
sourceFileCache,
types,
}: JavaScriptParserOptions = {},
) {
super({ctx, relativeTo});
Expand All @@ -108,7 +115,8 @@ export class JavaScriptParser extends Parser {
emitDecoratorMetadata: true,
forceConsistentCasingInFileNames: false,
jsx: ts.JsxEmit.Preserve,
baseUrl: relativeTo || process.cwd()
baseUrl: relativeTo || process.cwd(),
...(types ? {types} : {})
};
this.styles = styles;
this.sourceFileCache = sourceFileCache;
Expand Down
293 changes: 8 additions & 285 deletions rewrite-javascript/rewrite/src/javascript/recipes/change-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,9 @@ import { Option, Recipe } from "../../recipe";
import { TreeVisitor } from "../../visitor";
import { ExecutionContext } from "../../execution";
import { JavaScriptVisitor, JS } from "../index";
import { maybeAddImport } from "../add-import";
import { emptySpace, J, isIdentifier, rightPadded, singleSpace, Type } from "../../java";
import { create as produce, Draft } from "mutative";
import { randomId } from "../../uuid";
import { emptyMarkers } from "../../markers";

/**
* Binds `member` under the name `local` already carries, so the file's references to it still
* resolve. `local` itself becomes the alias, keeping the type attribution it holds, and the alias
* takes its prefix: that whitespace separates the specifier from a `type` keyword before it.
*/
function aliasing(local: Draft<J.Identifier>, member: string): JS.Alias {
const propertyName: J.Identifier = {
id: randomId(),
kind: J.Kind.Identifier,
prefix: emptySpace,
markers: emptyMarkers,
annotations: [],
simpleName: member,
type: undefined,
fieldType: undefined
};
return {
id: randomId(),
kind: JS.Kind.Alias,
prefix: local.prefix,
markers: emptyMarkers,
propertyName: rightPadded(propertyName, singleSpace),
alias: {...local, prefix: singleSpace} as J.Identifier
};
}
import { maybeRebind } from "../binding";
import { J, Type } from "../../java";
import { create as produce } from "mutative";

/**
* Changes an import from one module to another, updating all type attributions.
Expand Down Expand Up @@ -113,19 +85,11 @@ export class ChangeImport extends Recipe {
})
newMember?: string;

@Option({
displayName: "New alias",
description: "Optional alias for the new import. Required when newMember is 'default' or '*'.",
required: false
})
newAlias?: string;

constructor(options?: {
oldModule?: string;
oldMember?: string;
newModule?: string;
newMember?: string;
newAlias?: string;
}) {
super(options);
}
Expand All @@ -135,7 +99,6 @@ export class ChangeImport extends Recipe {
const oldMember = this.oldMember;
const newModule = this.newModule;
const newMember = this.newMember ?? oldMember;
const newAlias = this.newAlias;

// Build the old and new FQNs for type attribution updates
const oldFqn = oldMember === 'default' || oldMember === '*'
Expand All @@ -147,181 +110,14 @@ export class ChangeImport extends Recipe {

return new class extends JavaScriptVisitor<ExecutionContext> {
private hasOldImport = false;
private oldAlias?: string;
private transformedImport = false;

override async visitJsCompilationUnit(cu: JS.CompilationUnit, ctx: ExecutionContext): Promise<J | undefined> {
// Reset tracking for each file
this.hasOldImport = false;
this.oldAlias = undefined;
this.transformedImport = false;

// First pass: check if the old import exists and capture any alias
for (const statement of cu.statements) {
const stmt = statement.element ?? statement;
if (stmt.kind === JS.Kind.Import) {
const jsImport = stmt as JS.Import;
const aliasInfo = this.checkForOldImport(jsImport);
if (aliasInfo.found) {
this.hasOldImport = true;
this.oldAlias = aliasInfo.alias;
break;
}
}
}

// Visit the compilation unit (this will transform imports via visitJsImport)
let result = await super.visitJsCompilationUnit(cu, ctx) as JS.CompilationUnit;

// If we transformed an import but need to add to existing import from new module,
// or if we only removed a member from a multi-import, use maybeAddImport
if (this.hasOldImport && !this.transformedImport) {
const aliasToUse = newAlias ?? this.oldAlias;

if (newMember === 'default') {
maybeAddImport(this, {
module: newModule,
member: 'default',
alias: aliasToUse,
onlyIfReferenced: false
});
} else if (newMember === '*') {
maybeAddImport(this, {
module: newModule,
member: '*',
alias: aliasToUse,
onlyIfReferenced: false
});
} else {
maybeAddImport(this, {
module: newModule,
member: newMember,
// A pinned alias is taken verbatim: `oldMember` is the name this
// import bound, which the file's references to it already read.
alias: aliasToUse ?? oldMember,
onlyIfReferenced: false
});
}
}

return result;
}

override async visitImportDeclaration(jsImport: JS.Import, ctx: ExecutionContext): Promise<J | undefined> {
let imp = await super.visitImportDeclaration(jsImport, ctx) as JS.Import;
this.hasOldImport = maybeRebind(this, {
from: {module: oldModule, member: oldMember},
to: {module: newModule, member: newMember}
}) !== undefined;

if (!this.hasOldImport) {
return imp;
}

const aliasInfo = this.checkForOldImport(imp);
if (!aliasInfo.found) {
return imp;
}

// Check if this is the only import from the old module
const namedImports = this.getNamedImports(imp);
const isOnlyImport = namedImports.length === 1 ||
(oldMember === 'default' && !imp.importClause?.namedBindings) ||
(oldMember === '*');

if (isOnlyImport) {
// Transform the module specifier in place
this.transformedImport = true;
return produce(imp, draft => {
if (draft.moduleSpecifier) {
const literal = draft.moduleSpecifier.element as Draft<J.Literal>;
literal.value = newModule;
// Update valueSource to preserve quote style
const originalSource = literal.valueSource || `"${oldModule}"`;
const quoteChar = originalSource.startsWith("'") ? "'" : '"';
literal.valueSource = `${quoteChar}${newModule}${quoteChar}`;
}
// If we're also renaming the member, update the import specifier
if (newMember !== oldMember && oldMember !== 'default' && oldMember !== '*') {
const importClause = draft.importClause;
if (importClause?.namedBindings?.kind === JS.Kind.NamedImports) {
const namedImports = importClause.namedBindings as Draft<JS.NamedImports>;
for (const elem of namedImports.elements.elements) {
const specifier = elem.element;
if (specifier.specifier.kind === J.Kind.Identifier &&
specifier.specifier.simpleName === oldMember) {
specifier.specifier = aliasing(specifier.specifier as Draft<J.Identifier>, newMember);
} else if (specifier.specifier.kind === JS.Kind.Alias) {
const aliasNode = specifier.specifier as Draft<JS.Alias>;
const propertyName = aliasNode.propertyName.element;
if (propertyName.kind === J.Kind.Identifier &&
propertyName.simpleName === oldMember) {
propertyName.simpleName = newMember;
}
}
}
}
}
});
} else {
// Remove just the specific member from the import
// maybeAddImport will add the new import
return this.removeNamedImportMember(imp, oldMember, ctx);
}
}

private async removeNamedImportMember(imp: JS.Import, memberToRemove: string, _ctx: ExecutionContext): Promise<JS.Import> {
return produce(imp, draft => {
const importClause = draft.importClause;
if (!importClause?.namedBindings) return;
if (importClause.namedBindings.kind !== JS.Kind.NamedImports) return;

const namedImports = importClause.namedBindings as Draft<JS.NamedImports>;
const elements = namedImports.elements.elements;
const filteredElements = elements.filter(elem => {
const specifier = elem.element;
const specifierNode = specifier.specifier;

if (specifierNode.kind === J.Kind.Identifier) {
return specifierNode.simpleName !== memberToRemove;
}

if (specifierNode.kind === JS.Kind.Alias) {
const alias = specifierNode as JS.Alias;
const propertyName = alias.propertyName.element;
if (propertyName.kind === J.Kind.Identifier) {
return propertyName.simpleName !== memberToRemove;
}
}

return true;
});

namedImports.elements.elements = filteredElements;
});
}

private getNamedImports(imp: JS.Import): string[] {
const imports: string[] = [];
const importClause = imp.importClause;
if (!importClause) return imports;

const namedBindings = importClause.namedBindings;
if (!namedBindings || namedBindings.kind !== JS.Kind.NamedImports) return imports;

const namedImports = namedBindings as JS.NamedImports;
for (const elem of namedImports.elements.elements) {
const specifier = elem.element;
const specifierNode = specifier.specifier;

if (isIdentifier(specifierNode)) {
imports.push(specifierNode.simpleName);
} else if (specifierNode.kind === JS.Kind.Alias) {
const alias = specifierNode as JS.Alias;
const propertyName = alias.propertyName.element;
if (isIdentifier(propertyName)) {
imports.push(propertyName.simpleName);
}
}
}

return imports;
return super.visitJsCompilationUnit(cu, ctx);
}

override async visitIdentifier(identifier: J.Identifier, ctx: ExecutionContext): Promise<J | undefined> {
Expand Down Expand Up @@ -653,79 +449,6 @@ export class ChangeImport extends Recipe {
}
return arrayType;
}

private checkForOldImport(jsImport: JS.Import): { found: boolean; alias?: string } {
// Check if this import is from the old module
const moduleSpecifier = jsImport.moduleSpecifier;
if (!moduleSpecifier) return { found: false };

const literal = moduleSpecifier.element;
if (literal.kind !== J.Kind.Literal) return { found: false };

const value = (literal as J.Literal).value;
if (value !== oldModule) return { found: false };

const importClause = jsImport.importClause;
if (!importClause) {
// Side-effect import - not what we're looking for
return { found: false };
}

// Check for default import
if (oldMember === 'default') {
if (importClause.name) {
const nameElem = importClause.name.element;
if (isIdentifier(nameElem)) {
return { found: true, alias: nameElem.simpleName };
}
}
return { found: false };
}

// Check for namespace import
if (oldMember === '*') {
const namedBindings = importClause.namedBindings;
if (namedBindings?.kind === JS.Kind.Alias) {
const alias = namedBindings as JS.Alias;
if (isIdentifier(alias.alias)) {
return { found: true, alias: alias.alias.simpleName };
}
}
return { found: false };
}

// Check for named imports
const namedBindings = importClause.namedBindings;
if (!namedBindings) return { found: false };

if (namedBindings.kind !== JS.Kind.NamedImports) return { found: false };

const namedImports = namedBindings as JS.NamedImports;
const elements = namedImports.elements.elements;

for (const elem of elements) {
const specifier = elem.element;
const specifierNode = specifier.specifier;

// Handle direct import: import { act }
if (isIdentifier(specifierNode) && specifierNode.simpleName === oldMember) {
return { found: true };
}

// Handle aliased import: import { act as something }
if (specifierNode.kind === JS.Kind.Alias) {
const alias = specifierNode as JS.Alias;
const propertyName = alias.propertyName.element;
if (isIdentifier(propertyName) && propertyName.simpleName === oldMember) {
if (isIdentifier(alias.alias)) {
return { found: true, alias: alias.alias.simpleName };
}
}
}
}

return { found: false };
}
}();
}
}
Loading