Files
self/app/scripts/alias-imports.cjs
Justin Hernandez 8255a9ac56 chore: tighten types across mobile surface areas (#1209)
* chore(app): tighten types across app workspace

* fixes

* save wip

* save wip linting

* fix any types

* fix types

* fix: import forwardRef directly from react

- Changed from React.forwardRef to forwardRef import
- Resolves linting warning about named exports

* save typing and linting updates

* cr feedback. final pass

* more cr feedback

* pipeline fixes
2025-10-09 09:53:29 -07:00

220 lines
7.2 KiB
JavaScript

// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
const path = require('node:path');
const { Project, SyntaxKind } = require('ts-morph');
function determineAliasStrategy(dir, abs, baseDir, baseAlias) {
const rel = path.relative(baseDir, abs).replace(/\\/g, '/');
return rel ? `${baseAlias}/${rel}` : baseAlias;
}
function transformProjectToAliasImports(project, appRootPath) {
const srcDir = path.join(appRootPath, 'src');
const testsDir = path.join(appRootPath, 'tests', 'src');
const sourceFiles = project.getSourceFiles();
for (const sourceFile of sourceFiles) {
const dir = path.dirname(sourceFile.getFilePath());
// Handle import declarations
for (const declaration of sourceFile.getImportDeclarations()) {
try {
// Skip if no module specifier or not a string literal
const moduleSpecifier = declaration.getModuleSpecifier();
if (
!moduleSpecifier ||
moduleSpecifier.getKind() !== SyntaxKind.StringLiteral
) {
continue;
}
const spec = declaration.getModuleSpecifierValue();
// Skip existing alias imports
if (spec.startsWith('@/') || spec.startsWith('@tests/')) {
continue;
}
// Handle relative imports
if (!spec.startsWith('./') && !spec.startsWith('../')) continue;
const abs = path.resolve(dir, spec);
let baseDir = null;
let baseAlias = null;
// Determine containment safely using path.relative to avoid startsWith false positives
const relFromSrc = path.relative(srcDir, abs);
if (!relFromSrc.startsWith('..') && !path.isAbsolute(relFromSrc)) {
baseDir = srcDir;
baseAlias = '@';
} else {
const relFromTests = path.relative(testsDir, abs);
if (
!relFromTests.startsWith('..') &&
!path.isAbsolute(relFromTests)
) {
baseDir = testsDir;
baseAlias = '@tests';
} else {
continue;
}
}
const newSpec = determineAliasStrategy(dir, abs, baseDir, baseAlias);
declaration.setModuleSpecifier(newSpec);
} catch (error) {
// Skip declarations that can't be processed (e.g., type-only imports with issues)
const msg = error instanceof Error ? error.message : String(error);
console.warn(
`Skipping import declaration in ${sourceFile.getFilePath()}: ${msg}`,
);
try {
console.debug('Import declaration text:', declaration.getText());
} catch {}
if (error && typeof error === 'object' && 'stack' in error) {
console.debug('Error stack:', error.stack);
}
continue;
}
}
// Handle export declarations like: export * from '../x' or export {A} from '../x'
for (const declaration of sourceFile.getExportDeclarations()) {
try {
// Skip if no module specifier or not a string literal
const moduleSpecifier = declaration.getModuleSpecifier();
if (
!moduleSpecifier ||
moduleSpecifier.getKind() !== SyntaxKind.StringLiteral
) {
continue;
}
const spec = declaration.getModuleSpecifierValue();
if (!spec) continue;
// Skip existing alias exports
if (spec.startsWith('@/') || spec.startsWith('@tests/')) {
continue;
}
// Handle relative exports
if (!spec.startsWith('./') && !spec.startsWith('../')) continue;
const abs = path.resolve(dir, spec);
let baseDir = null;
let baseAlias = null;
const relFromSrc = path.relative(srcDir, abs);
if (!relFromSrc.startsWith('..') && !path.isAbsolute(relFromSrc)) {
baseDir = srcDir;
baseAlias = '@';
} else {
const relFromTests = path.relative(testsDir, abs);
if (
!relFromTests.startsWith('..') &&
!path.isAbsolute(relFromTests)
) {
baseDir = testsDir;
baseAlias = '@tests';
} else {
continue;
}
}
const newSpec = determineAliasStrategy(dir, abs, baseDir, baseAlias);
declaration.setModuleSpecifier(newSpec);
} catch (error) {
// Skip declarations that can't be processed
const msg = error instanceof Error ? error.message : String(error);
console.warn(
`Skipping export declaration in ${sourceFile.getFilePath()}: ${msg}`,
);
try {
console.debug('Export declaration text:', declaration.getText());
} catch {}
if (error && typeof error === 'object' && 'stack' in error) {
console.debug('Error stack:', error.stack);
}
continue;
}
}
// Handle require() calls
const requireCalls = sourceFile.getDescendantsOfKind(
SyntaxKind.CallExpression,
);
for (const call of requireCalls) {
const expression = call.getExpression();
const exprText = expression.getText();
const isRequire = exprText === 'require';
const isDynamicImport = exprText === 'import';
const isJestMock =
exprText === 'jest.mock' ||
exprText === 'jest.doMock' ||
exprText === 'jest.unmock';
if (!isRequire && !isDynamicImport && !isJestMock) continue;
const args = call.getArguments();
if (args.length === 0) continue;
const arg = args[0];
if (arg.getKind() !== SyntaxKind.StringLiteral) continue;
const spec = arg.getLiteralValue();
// Skip existing alias requires
if (spec.startsWith('@/') || spec.startsWith('@tests/')) {
continue;
}
// Handle relative requires
if (!spec.startsWith('./') && !spec.startsWith('../')) continue;
const abs = path.resolve(dir, spec);
let baseDir = null;
let baseAlias = null;
// Determine containment safely using path.relative to avoid startsWith false positives
const relFromSrc = path.relative(srcDir, abs);
if (!relFromSrc.startsWith('..') && !path.isAbsolute(relFromSrc)) {
baseDir = srcDir;
baseAlias = '@';
} else {
const relFromTests = path.relative(testsDir, abs);
if (!relFromTests.startsWith('..') && !path.isAbsolute(relFromTests)) {
baseDir = testsDir;
baseAlias = '@tests';
} else {
continue;
}
}
const newSpec = determineAliasStrategy(dir, abs, baseDir, baseAlias);
arg.setLiteralValue(newSpec);
}
}
}
function runAliasImportsTransform(options = {}) {
const appRoot = options.appRoot || path.resolve(__dirname, '..');
const project =
options.project ||
new Project({ tsConfigFilePath: path.join(appRoot, 'tsconfig.json') });
// Include test files since they're excluded in tsconfig
if (!options.skipAddTests) {
project.addSourceFilesAtPaths(['tests/**/*.{ts,tsx}']);
}
transformProjectToAliasImports(project, appRoot);
project.saveSync();
return project;
}
if (require.main === module) {
runAliasImportsTransform();
}
module.exports = { runAliasImportsTransform, transformProjectToAliasImports };