Skip to content

Commit

Permalink
feat(compiler): transform module aliases in emitted js, typedefs
Browse files Browse the repository at this point in the history
This implements a transformer which is very similar to the one added
in #3523 but which is run earlier in the build process and which can
therefore rewrite aliased paths in both emitted JS and typedef files.

This matters if the user has the `generateTypeDeclarations` option set
to `true` on one of their output targets.

The new behavior implemented here, however, is no longer specific to a
particular output target, and applies to all TypeScript code which runs
through Stencil. Accordingly, the behavior is opt-in, and is controlled
by a new configuration value, `transformAliasedImportPaths` which
defaults to `false`.
  • Loading branch information
alicewriteswrongs committed Feb 13, 2023
1 parent d3997d0 commit 36e4073
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 3 deletions.
1 change: 0 additions & 1 deletion src/compiler/sys/modules/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './compiler-deps';
export * from './crypto';
export * from './dts-core';
export * from './dts-internal';
Expand Down
149 changes: 149 additions & 0 deletions src/compiler/transformers/rewrite_aliased_paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { normalizePath } from '@utils';
import { dirname, relative } from 'path';
import ts from 'typescript';
import { retrieveTsModifiers } from './transform-utils';

/**
* Transform modules aliased with `paths` in `tsconfig.json` to relative
* imported in `.d.ts` files.
*
* @returns a TypeScript transformer factory
*/
export function rewriteAliasedDTSPaths(): ts.TransformerFactory<ts.Bundle | ts.SourceFile> {
return (transformCtx: ts.TransformationContext) => {
const compilerHost = ts.createCompilerHost(transformCtx.getCompilerOptions());

return (tsBundleOrSourceFile) => {
let fileName = ts.isBundle(tsBundleOrSourceFile)
? tsBundleOrSourceFile.getSourceFile().fileName
: tsBundleOrSourceFile.fileName;

return ts.visitEachChild(tsBundleOrSourceFile, visit(compilerHost, transformCtx, fileName), transformCtx);
};
};
}

/**
* Transform modules aliased with `paths` in `tsconfig.json` to relative
* imported in source files.
*
* @returns a TypeScript transformer factory
*/
export const rewriteAliasedSourceFilePaths = (): ts.TransformerFactory<ts.SourceFile> => {
return (transformCtx: ts.TransformationContext) => {
const compilerHost = ts.createCompilerHost(transformCtx.getCompilerOptions())

return (tsSourceFile) => {
return ts.visitEachChild(
tsSourceFile,
visit(compilerHost, transformCtx, tsSourceFile.fileName),
transformCtx
);
};
};
};

/**
* This visitor function will modify any {@link ts.ImportDeclaration} nodes to
* rewrite module identifiers which are configured using
* the `paths` parameter in `tsconfig.json` from whatever name they are bound to
* to a relative path from the importer to the importee.
*
* We need to handle this ourselves because while the TypeScript team supports
* using the `paths` configuration to allow location-independent imports across
* a project (i.e. importing a module without having to use its relative
* path from the importing module) the TypeScript compiler has no built-in
* support for resolving these identifiers to the actual modules they point to
* in the `.js` and `.d.ts` files that it emits.
*
* So, for instance, if you have this set in `paths`:
*
* ```json
* "paths": {
* "@utils": ["src/utils/index.ts""],
* }
* ```
*
* Then you'll be able to import it anywhere in your project:
*
* ```ts
* // src/importing.ts
* import { myUtil } from '@utils';
* ```
*
* but unfortunately, in the compiled output you'll still have:
*
* ```js
* // dist/importing.js
* import { myUtil } from "@utils";
* ```
*
* instead of what you _most likely_ want, which is:
*
* ```js
* // dist/importing.js
* import { myUtil } from "./utils";
* ```
*
* The TypeScript team have stated pretty unequivocally that they will not
* automatically resolve these identifiers to relative paths in output code
* {@see https://github.com/microsoft/TypeScript/issues/10866} and have
* said that resolving these module identifiers is the responsability of module
* bundling and build tools.
*
* So that means we've got to do it!
*
* This visitor function does so by getting the resolved file path to any
* module which is not 1) not external (i.e. not a dependency) and 2) is not
* already a relative, file-path based import. It then replaces the module
* identifier with the relative path from the importer to the importee.
*/
const visit = (
compilerHost: ts.CompilerHost,
transformCtx: ts.TransformationContext,
sourceFilePath: string
) => {
return (node: ts.Node): ts.VisitResult<ts.Node> => {
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
let importPath = node.moduleSpecifier.text;

// We will ignore transforming any paths that are already relative paths or
// imports from external modules/packages
if (!importPath.startsWith('.')) {
const module = ts.resolveModuleName(importPath, sourceFilePath, transformCtx.getCompilerOptions(), compilerHost);

const hasResolvedFileName = module.resolvedModule?.resolvedFileName != null;
const isModuleFromNodeModules = module.resolvedModule?.isExternalLibraryImport === true;
const shouldTranspileImportPath = hasResolvedFileName && !isModuleFromNodeModules;

if (shouldTranspileImportPath) {
// Create a regular expression that will be used to remove the last file extension
// from the import path
const extensionRegex = new RegExp(
Object.values(ts.Extension)
.map((extension) => `${extension}$`)
.join('|')
);

// In order to make sure the relative path works when the destination depth is different than the source
// file structure depth, we need to determine where the resolved file exists relative to the destination directory
const resolvePathInDestination = module.resolvedModule.resolvedFileName;

importPath = normalizePath(
relative(dirname(sourceFilePath), resolvePathInDestination).replace(extensionRegex, '')
);
}
}

return transformCtx.factory.updateImportDeclaration(
node,
retrieveTsModifiers(node),
node.importClause,
transformCtx.factory.createStringLiteral(importPath),
node.assertClause
);
}

return ts.visitEachChild(node, visit(compilerHost, transformCtx, sourceFilePath), transformCtx);
};
};
9 changes: 7 additions & 2 deletions src/compiler/transpile/run-program.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { loadTypeScriptDiagnostics, normalizePath } from '@utils';
import { basename, join, relative } from 'path';
import type ts from 'typescript';
import ts from 'typescript';

import type * as d from '../../declarations';
import { updateComponentBuildConditionals } from '../app-core/app-data';
import { resolveComponentDependencies } from '../entries/resolve-component-dependencies';
import { getComponentsFromModules, isOutputTargetDistTypes } from '../output-targets/output-utils';
import { convertDecoratorsToStatic } from '../transformers/decorators-to-static/convert-decorators';
import { updateModule } from '../transformers/static-to-meta/parse-static';
import { rewriteAliasedDTSPaths, rewriteAliasedSourceFilePaths } from '../transformers/rewrite_aliased_paths';
import { generateAppTypes } from '../types/generate-app-types';
import { updateStencilTypesImports } from '../types/stencil-types';
import { validateTranspiledComponents } from './validate-components';
Expand Down Expand Up @@ -52,7 +53,11 @@ export const runTsProgram = async (

// Emit files that changed
tsBuilder.emit(undefined, emitCallback, undefined, false, {
before: [convertDecoratorsToStatic(config, buildCtx.diagnostics, tsTypeChecker)],
before: [
convertDecoratorsToStatic(config, buildCtx.diagnostics, tsTypeChecker),
rewriteAliasedSourceFilePaths(),
],
afterDeclarations: [rewriteAliasedDTSPaths()],
});

const changedmodules = Array.from(compilerCtx.changedModules.keys());
Expand Down

0 comments on commit 36e4073

Please sign in to comment.