Skip to content

Commit

Permalink
fix(babel): support type imports without type annotation (#1282)
Browse files Browse the repository at this point in the history
  • Loading branch information
Anber committed Jul 14, 2023
1 parent cceaac9 commit 85e74df
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 47 deletions.
7 changes: 7 additions & 0 deletions .changeset/kind-roses-deny.md
@@ -0,0 +1,7 @@
---
'@linaria/shaker': patch
'@linaria/testkit': patch
'@linaria/utils': patch
---

Fix: type imports without `type` annotation may lead to an unexpected increase in the evaluated codebase.
74 changes: 47 additions & 27 deletions packages/shaker/src/plugins/__tests__/shaker-plugin.test.ts
@@ -1,42 +1,62 @@
import { join } from 'path';

import type { PluginItem } from '@babel/core';
import { transformSync } from '@babel/core';
import dedent from 'dedent';

import shakerPlugin, { hasShakerMetadata } from '../shaker-plugin';

const keep = (only: string[]) => (code: TemplateStringsArray) => {
const filename = join(__dirname, 'source.js');
const formattedCode = dedent(code);

const transformed = transformSync(formattedCode, {
babelrc: false,
configFile: false,
filename,
plugins: [
[
shakerPlugin,
{
onlyExports: only,
},
],
],
});
type Extension = 'js' | 'ts' | 'jsx' | 'tsx';

if (
!transformed ||
!transformed.code ||
!hasShakerMetadata(transformed.metadata)
) {
throw new Error(`${filename} has no shaker metadata`);
const getPresets = (extension: Extension) => {
const presets: PluginItem[] = [];
if (extension === 'ts' || extension === 'tsx') {
presets.push(require.resolve('@babel/preset-typescript'));
}

return {
code: transformed.code,
metadata: transformed.metadata.__linariaShaker,
};
if (extension === 'jsx' || extension === 'tsx') {
presets.push(require.resolve('@babel/preset-react'));
}

return presets;
};

const keep =
(only: string[], extension: Extension = 'js') =>
(code: TemplateStringsArray) => {
const presets = getPresets(extension);
const filename = join(__dirname, `source.${extension}`);
const formattedCode = dedent(code);

const transformed = transformSync(formattedCode, {
babelrc: false,
configFile: false,
filename,
presets,
plugins: [
[
shakerPlugin,
{
onlyExports: only,
},
],
],
});

if (
!transformed ||
!transformed.code ||
!hasShakerMetadata(transformed.metadata)
) {
throw new Error(`${filename} has no shaker metadata`);
}

return {
code: transformed.code,
metadata: transformed.metadata.__linariaShaker,
};
};

describe('shaker', () => {
it('should remove unused export', () => {
const { code, metadata } = keep(['a'])`
Expand Down
3 changes: 3 additions & 0 deletions packages/testkit/src/__fixtures__/linaria-ui-library/types.ts
@@ -0,0 +1,3 @@
export type ComponentType = 'class' | 'function' | 'arrow';

export Unexpected token;
34 changes: 34 additions & 0 deletions packages/testkit/src/__snapshots__/babel.test.ts.snap
Expand Up @@ -2062,6 +2062,40 @@ Dependencies: NA
`;
exports[`strategy shaker should not import types 1`] = `
"import { styled } from \\"@linaria/react\\";
import { Title } from \\"./__fixtures__/linaria-ui-library/components/index\\";
import { ComponentType } from \\"./__fixtures__/linaria-ui-library/types\\";
const map = new Map<string, ComponentType>().set('Title', Title);
const Gate = (props: {
type: ComponentType;
className: string;
}) => {
const {
className,
type
} = props;
const Component = map.get(type);
return <Component className={className} />;
};
const _exp = /*#__PURE__*/() => Gate;
export const StyledTitle = /*#__PURE__*/styled(_exp())({
name: \\"StyledTitle\\",
class: \\"StyledTitle_s18alru3\\",
propsAsIs: true
});"
`;
exports[`strategy shaker should not import types 2`] = `
CSS:
.StyledTitle_s18alru3 {}
Dependencies: NA
`;
exports[`strategy shaker should process \`css\` calls inside components 1`] = `
"import React from 'react';
export function Component() {
Expand Down
25 changes: 25 additions & 0 deletions packages/testkit/src/babel.test.ts
Expand Up @@ -2799,4 +2799,29 @@ describe('strategy shaker', () => {
expect(code).toMatchSnapshot();
expect(metadata).toMatchSnapshot();
});

it('should not import types', async () => {
const { code, metadata } = await transform(
dedent`
import { styled } from "@linaria/react";
import { Title } from "./__fixtures__/linaria-ui-library/components/index";
import { ComponentType } from "./__fixtures__/linaria-ui-library/types";
const map = new Map<string, ComponentType>()
.set('Title', Title);
const Gate = (props: { type: ComponentType, className: string }) => {
const { className, type } = props;
const Component = map.get(type);
return <Component className={className}/>;
};
export const StyledTitle = styled(Gate)\`\`;
`,
[evaluator, {}, 'tsx']
);

expect(code).toMatchSnapshot();
expect(metadata).toMatchSnapshot();
});
});
10 changes: 0 additions & 10 deletions packages/utils/src/findIdentifiers.ts
Expand Up @@ -49,11 +49,6 @@ export default function findIdentifiers(
return;
}

if (!nonType(path)) {
// If skip in TSTypeAnnotation visitor doesn't work
return;
}

// TODO: Is there a better way to check that it's a local variable?

const binding = getScope(path).getBinding(path.node.name);
Expand All @@ -73,11 +68,6 @@ export default function findIdentifiers(
emit(ex);
} else {
ex.traverse({
TSTypeAnnotation(path) {
// We ignore identifiers in type annotations
// It will produce broken TS code, but we don't care
path.skip();
},
Identifier(path: NodePath<Identifier>) {
emit(path);
},
Expand Down
16 changes: 6 additions & 10 deletions packages/utils/src/isRemoved.ts
Expand Up @@ -20,22 +20,18 @@ export default function isRemoved(path: NodePath): boolean {
return true;
}

const { listKey, key } = currentPath;
const { listKey, key, node } = currentPath;
if (listKey) {
// If the current path is part of a list and its node is not the same
// as the node in the parent list at the same index, return true
if (
(parent.get(listKey) as NodePath[])[key as number].node !==
currentPath.node
) {
// If the current path is part of a list and the current node
// is not presented in this list, return true
const found = parent.get(listKey).find((p) => p.node === node);
if (!found) {
return true;
}
}
// If the current path is not part of a list and its node is not the same
// as the node in the parent object at the same key, return true
else if (
(parent.get(key as string) as NodePath).node !== currentPath.node
) {
else if ((parent.get(key as string) as NodePath).node !== node) {
return true;
}
}
Expand Down

0 comments on commit 85e74df

Please sign in to comment.