Skip to content

Commit

Permalink
fix(collection): properly transform imports (#3523)
Browse files Browse the repository at this point in the history
This commit adds a transformer to the transpilation process to
resolve module imports and replace path aliases with
auto-generated relative paths for `dist-collection` when
`tsconfig.json#paths` is defined.

Prior to this, the `dist-collection` output target would not
transpile path aliases defined in a project's `tsconfig.json`.
Instead, the import paths were left unchanged and thus
reference modules that cannot be resolved.

STENCIL-437 Output target collection does not transpile ts path config
  • Loading branch information
tanner-reits committed Sep 8, 2022
1 parent 965323b commit ac2c09e
Show file tree
Hide file tree
Showing 10 changed files with 608 additions and 11 deletions.
3 changes: 2 additions & 1 deletion src/compiler/config/outputs/validate-collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export const validateCollection = (
return userOutputs.filter(isOutputTargetDistCollection).map((outputTarget) => {
return {
...outputTarget,
dir: getAbsolutePath(config, outputTarget.dir || 'dist/collection'),
transformAliasedImportPaths: outputTarget.transformAliasedImportPaths ?? false,
dir: getAbsolutePath(config, outputTarget.dir ?? 'dist/collection'),
};
});
};
2 changes: 2 additions & 0 deletions src/compiler/config/outputs/validate-dist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const validateDist = (config: d.ValidatedConfig, userOutputs: d.OutputTar
dir: distOutputTarget.dir,
collectionDir: distOutputTarget.collectionDir,
empty: distOutputTarget.empty,
transformAliasedImportPaths: distOutputTarget.transformAliasedImportPathsInCollection,
});
outputs.push({
type: COPY,
Expand Down Expand Up @@ -134,6 +135,7 @@ const validateOutputTargetDist = (config: d.ValidatedConfig, o: d.OutputTargetDi
copy: validateCopy(o.copy ?? [], []),
polyfills: isBoolean(o.polyfills) ? o.polyfills : undefined,
empty: isBoolean(o.empty) ? o.empty : true,
transformAliasedImportPathsInCollection: o.transformAliasedImportPathsInCollection ?? false,
};

if (!isAbsolute(outputTarget.buildDir)) {
Expand Down
87 changes: 87 additions & 0 deletions src/compiler/config/test/validate-output-dist-collection.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type * as d from '@stencil/core/declarations';
import { validateConfig } from '../validate-config';
import { mockConfig, mockLoadConfigInit } from '@stencil/core/testing';
import { resolve, join } from 'path';

describe('validateDistCollectionOutputTarget', () => {
let config: d.Config;

const rootDir = resolve('/');
const defaultDir = join(rootDir, 'dist', 'collection');

beforeEach(() => {
config = mockConfig();
});

it('sets correct default values', () => {
const target: d.OutputTargetDistCollection = {
type: 'dist-collection',
empty: false,
dir: null,
collectionDir: null,
};
config.outputTargets = [target];

const { config: validatedConfig } = validateConfig(config, mockLoadConfigInit());

expect(validatedConfig.outputTargets).toEqual([
{
type: 'dist-collection',
empty: false,
dir: defaultDir,
collectionDir: null,
transformAliasedImportPaths: false,
},
]);
});

it('sets specified directory', () => {
const target: d.OutputTargetDistCollection = {
type: 'dist-collection',
empty: false,
dir: '/my-dist',
collectionDir: null,
};
config.outputTargets = [target];

const { config: validatedConfig } = validateConfig(config, mockLoadConfigInit());

expect(validatedConfig.outputTargets).toEqual([
{
type: 'dist-collection',
empty: false,
dir: '/my-dist',
collectionDir: null,
transformAliasedImportPaths: false,
},
]);
});

describe('transformAliasedImportPaths', () => {
it.each([false, true])(
"sets option '%s' when explicitly '%s' in config",
(transformAliasedImportPaths: boolean) => {
const target: d.OutputTargetDistCollection = {
type: 'dist-collection',
empty: false,
dir: null,
collectionDir: null,
transformAliasedImportPaths,
};
config.outputTargets = [target];

const { config: validatedConfig } = validateConfig(config, mockLoadConfigInit());

expect(validatedConfig.outputTargets).toEqual([
{
type: 'dist-collection',
empty: false,
dir: defaultDir,
collectionDir: null,
transformAliasedImportPaths,
},
]);
}
);
});
});
88 changes: 88 additions & 0 deletions src/compiler/config/test/validate-output-dist.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe('validateDistOutputTarget', () => {
type: 'dist',
polyfills: undefined,
typesDir: path.join(rootDir, 'my-dist', 'types'),
transformAliasedImportPathsInCollection: false,
},
{
esmDir: path.join(rootDir, 'my-dist', 'my-build', 'testing'),
Expand Down Expand Up @@ -62,6 +63,7 @@ describe('validateDistOutputTarget', () => {
collectionDir: path.join(rootDir, 'my-dist', 'collection'),
dir: path.join(rootDir, '/my-dist'),
empty: false,
transformAliasedImportPaths: false,
type: 'dist-collection',
},
{
Expand Down Expand Up @@ -107,4 +109,90 @@ describe('validateDistOutputTarget', () => {
const { config } = validateConfig(userConfig, mockLoadConfigInit());
expect(config.outputTargets.some((o) => o.type === 'dist')).toBe(false);
});

it('sets option to transform aliased import paths when enabled', () => {
const outputTarget: d.OutputTargetDist = {
type: 'dist',
dir: 'my-dist',
buildDir: 'my-build',
empty: false,
transformAliasedImportPathsInCollection: true,
};
userConfig.outputTargets = [outputTarget];
userConfig.buildDist = true;

const { config } = validateConfig(userConfig, mockLoadConfigInit());

expect(config.outputTargets).toEqual([
{
buildDir: path.join(rootDir, 'my-dist', 'my-build'),
collectionDir: path.join(rootDir, 'my-dist', 'collection'),
copy: [],
dir: path.join(rootDir, 'my-dist'),
empty: false,
esmLoaderPath: path.join(rootDir, 'my-dist', 'loader'),
type: 'dist',
polyfills: undefined,
typesDir: path.join(rootDir, 'my-dist', 'types'),
transformAliasedImportPathsInCollection: true,
},
{
esmDir: path.join(rootDir, 'my-dist', 'my-build', 'testing'),
empty: false,
isBrowserBuild: true,
legacyLoaderFile: path.join(rootDir, 'my-dist', 'my-build', 'testing.js'),
polyfills: true,
systemDir: undefined,
systemLoaderFile: undefined,
type: 'dist-lazy',
},
{
copyAssets: 'dist',
copy: [],
dir: path.join(rootDir, 'my-dist', 'my-build', 'testing'),
type: 'copy',
},
{
file: path.join(rootDir, 'my-dist', 'my-build', 'testing', 'testing.css'),
type: 'dist-global-styles',
},
{
dir: path.join(rootDir, 'my-dist'),
type: 'dist-types',
typesDir: path.join(rootDir, 'my-dist', 'types'),
},
{
collectionDir: path.join(rootDir, 'my-dist', 'collection'),
dir: path.join(rootDir, '/my-dist'),
empty: false,
transformAliasedImportPaths: true,
type: 'dist-collection',
},
{
copy: [{ src: '**/*.svg' }, { src: '**/*.js' }],
copyAssets: 'collection',
dir: path.join(rootDir, 'my-dist', 'collection'),
type: 'copy',
},
{
type: 'dist-lazy',
cjsDir: path.join(rootDir, 'my-dist', 'cjs'),
cjsIndexFile: path.join(rootDir, 'my-dist', 'index.cjs.js'),
empty: false,
esmDir: path.join(rootDir, 'my-dist', 'esm'),
esmEs5Dir: undefined,
esmIndexFile: path.join(rootDir, 'my-dist', 'index.js'),
polyfills: true,
},
{
cjsDir: path.join(rootDir, 'my-dist', 'cjs'),
componentDts: path.join(rootDir, 'my-dist', 'types', 'components.d.ts'),
dir: path.join(rootDir, 'my-dist', 'loader'),
empty: false,
esmDir: path.join(rootDir, 'my-dist', 'esm'),
esmEs5Dir: undefined,
type: 'dist-lazy-loader',
},
]);
});
});
39 changes: 34 additions & 5 deletions src/compiler/output-targets/dist-collection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,20 @@ import { catchError, COLLECTION_MANIFEST_FILE_NAME, flatOne, generatePreamble, n
import { isOutputTargetDistCollection } from '../output-utils';
import { join, relative } from 'path';
import { typescriptVersion, version } from '../../../version';
import ts from 'typescript';
import { mapImportsToPathAliases } from '../../transformers/map-imports-to-path-aliases';

/**
* Main output target function for `dist-collection`. This function takes the compiled output from a
* {@link ts.Program}, runs each file through a transformer to transpile import path aliases, and then writes
* the output code and source maps to disk in the specified collection directory.
*
* @param config The validated Stencil config.
* @param compilerCtx The current compiler context.
* @param buildCtx The current build context.
* @param changedModuleFiles The changed modules returned from the TS compiler.
* @returns An empty promise. Resolved once all functions finish.
*/
export const outputCollection = async (
config: d.ValidatedConfig,
compilerCtx: d.CompilerCtx,
Expand All @@ -27,15 +40,31 @@ export const outputCollection = async (
const mapCode = mod.sourceMapFileText;

await Promise.all(
outputTargets.map(async (o) => {
outputTargets.map(async (target) => {
const relPath = relative(config.srcDir, mod.jsFilePath);
const filePath = join(o.collectionDir, relPath);
await compilerCtx.fs.writeFile(filePath, code, { outputTargetType: o.type });
const filePath = join(target.collectionDir, relPath);

// Transpile the already transpiled modules to apply
// a transformer to convert aliased import paths to relative paths
// We run this even if the transformer will perform no action
// to avoid race conditions between multiple output targets that
// may be writing to the same location
const { outputText } = ts.transpileModule(code, {
fileName: mod.sourceFilePath,
compilerOptions: {
target: ts.ScriptTarget.Latest,
},
transformers: {
after: [mapImportsToPathAliases(config, filePath, target)],
},
});

await compilerCtx.fs.writeFile(filePath, outputText, { outputTargetType: target.type });

if (mod.sourceMapPath) {
const relativeSourceMapPath = relative(config.srcDir, mod.sourceMapPath);
const sourceMapOutputFilePath = join(o.collectionDir, relativeSourceMapPath);
await compilerCtx.fs.writeFile(sourceMapOutputFilePath, mapCode, { outputTargetType: o.type });
const sourceMapOutputFilePath = join(target.collectionDir, relativeSourceMapPath);
await compilerCtx.fs.writeFile(sourceMapOutputFilePath, mapCode, { outputTargetType: target.type });
}
})
);
Expand Down
73 changes: 73 additions & 0 deletions src/compiler/output-targets/test/output-targets-collection.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { outputCollection } from '../dist-collection';
import type * as d from '../../../declarations';
import { mockValidatedConfig, mockBuildCtx, mockCompilerCtx, mockModule } from '@stencil/core/testing';
import * as test from '../../transformers/map-imports-to-path-aliases';
import { normalize } from 'path';

describe('Dist Collection output target', () => {
let mockConfig: d.ValidatedConfig;
let mockedBuildCtx: d.BuildCtx;
let mockedCompilerCtx: d.CompilerCtx;
let changedModules: d.Module[];

let mapImportPathSpy: jest.SpyInstance;

const mockTraverse = jest.fn().mockImplementation((source: any) => source);
const mockMap = jest.fn().mockImplementation(() => mockTraverse);
const target: d.OutputTargetDistCollection = {
type: 'dist-collection',
dir: '',
collectionDir: '/dist/collection',
};

beforeEach(() => {
mockConfig = mockValidatedConfig({
srcDir: '/src',
});
mockedBuildCtx = mockBuildCtx();
mockedCompilerCtx = mockCompilerCtx();
changedModules = [
mockModule({
staticSourceFileText: '',
jsFilePath: '/src/main.js',
sourceFilePath: '/src/main.ts',
}),
];

jest.spyOn(mockedCompilerCtx.fs, 'writeFile');

mapImportPathSpy = jest.spyOn(test, 'mapImportsToPathAliases');
mapImportPathSpy.mockReturnValue(mockMap);
});

afterEach(() => {
jest.restoreAllMocks();
});

describe('transform aliased import paths', () => {
// These tests ensure that the transformer for import paths is called regardless
// of the config value (the function will decided whether or not to actually do anything) to avoid
// a race condition with duplicate file writes
it.each([true, false])(
'calls function to transform aliased import paths when the output target config flag is `%s`',
async (transformAliasedImportPaths: boolean) => {
mockConfig.outputTargets = [
{
...target,
transformAliasedImportPaths,
},
];

await outputCollection(mockConfig, mockedCompilerCtx, mockedBuildCtx, changedModules);

expect(mapImportPathSpy).toHaveBeenCalledWith(mockConfig, normalize('/dist/collection/main.js'), {
collectionDir: '/dist/collection',
dir: '',
transformAliasedImportPaths,
type: 'dist-collection',
});
expect(mapImportPathSpy).toHaveBeenCalledTimes(1);
}
);
});
});
Loading

0 comments on commit ac2c09e

Please sign in to comment.