All files / src/transformers isolated-component-files.ts

100% Statements 24/24
100% Branches 29/29
100% Functions 6/6
100% Lines 22/22

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147            113x           56x 5x                                     51x                                                                                                                                     113x                 77x 103x 103x 103x     103x         103x 41x     171x 62x   56x 56x   56x   56x   109x       56x           56x 56x    
/**
 * @module transformers
 */
import * as ts from "typescript";
 
/** becomes `import { executeCyTests } from "orphic-cypress"` */
const createImportStatement = (
  factory: ts.NodeFactory,
  opts: ts.CompilerOptions,
  executeCyTestsLocation: string
) => {
  // handle commonjs module type as chosen in tsconfig.json
  if (opts.module === ts.ModuleKind.CommonJS) {
    return factory.createVariableStatement(
      /* modifiers */ undefined,
      factory.createVariableDeclarationList([
        factory.createVariableDeclaration(
          "executeCyTests",
          /* exclamation token */ undefined,
          /* type */ undefined,
          factory.createPropertyAccessExpression(
            factory.createCallExpression(
              factory.createIdentifier("require"),
              /* type arguments */ undefined,
              [factory.createStringLiteral(executeCyTestsLocation)]
            ),
            factory.createIdentifier("executeCyTests")
          )
        ),
      ])
    );
  }
  return factory.createImportDeclaration(
    /* modifiers */ undefined,
    factory.createImportClause(
      /* isTypeOnly */ false,
      /* name (so default import) */ undefined,
      factory.createNamedImports([
        factory.createImportSpecifier(
          /* isTypeOnly */ false,
          /* name (would mean `export { executeCyTests as })`) */ undefined,
          factory.createIdentifier("executeCyTests")
        ),
      ])
    ),
    factory.createStringLiteral(executeCyTestsLocation)
  );
};
 
type Source = ts.SourceFile & {
  symbol: {
    exports: Map<string, { declarations: ts.ExportAssignment[] }>;
  };
};
 
/**
 * Transform a typescript stories file by adding `executeCyTests` to the bottom
 * with all exports explicitly passed in and the default recreated to be passed
 * in as 'default' prop.
 *
 * In webpack, can use with ts-loader like so
 * ```ts
 * {
 *   test: /\.[jt]sx?$/,
 *   exclude: [/node_modules/],
 *   use: [
 *     {
 *       loader: "ts-loader",
 *       options: {
 *         happyPackMode: true,
 *         transpileOnly: true,
 *         ...(useIsolatedComponentFiles && {
 *           getCustomTransformers: () => ({
 *             before: [transformIsolatedComponentFiles()],
 *           }),
 *         }),
 *       },
 *     },
 *   ],
 * }
 * ```
 *
 * To include mdx files as tests, add this to module rules
 * ```ts
 * {
 *   test: /\.mdx$/,
 *   use: [
 *     <above ts-loader config>,
 *     {
 *       loader: require.resolve("@storybook/mdx1-csf/loader"),
 *       options: { skipCsf },
 *     },
 *   ],
 * },
 * ```
 * see {@link bundlers.cypressWebpackConfigMdx} for an abstraction to do just that,
 * as well as load non-story mdx and md files.
 */
export const transformIsolatedComponentFiles =
  (
    /**
     * Location for `executeCyTests`. Defaults to this module, but you could import it elsewhere
     * and change via pre/post call, or rewrite entirely and point to it from here
     */
    executeCyTestsLocation = "orphic-cypress",
    /** story filename pattern */
    storyPattern: string | RegExp = /\.stories|story\./
  ): ts.TransformerFactory<ts.SourceFile> =>
  (context) =>
  (source) => {
    const exports = (source as Source).symbol?.exports ?? new Map();
    const defaultExport = exports.get("default")?.declarations?.[0];
 
    const matches =
      source.fileName &&
      (storyPattern instanceof RegExp
        ? storyPattern.test(source.fileName)
        : source.fileName.includes(storyPattern));
    // docs only stories will have a ___page which intentionally throws an error
    if (!matches || !exports || exports.has("___page") || !defaultExport) {
      return source;
    }
 
    const exportKeys = [...exports.keys()].filter((name) => name !== "default");
    if (!exportKeys.length) return source;
 
    const { factory: f, getCompilerOptions } = context;
    const opts = getCompilerOptions();
 
    const newImport = createImportStatement(f, opts, executeCyTestsLocation);
 
    const newExportObject = f.createObjectLiteralExpression([
      f.createPropertyAssignment("default", defaultExport.expression),
      ...exportKeys.map((k) => f.createShorthandPropertyAssignment(k)),
    ]);
 
    /** becomes `executeCyCall({ default: {<default export obj>}, OtherStory, <...> })` */
    const executeCyCall = f.createCallExpression(
      f.createIdentifier("executeCyTests"),
      [],
      [newExportObject]
    ) as any as ts.Statement;
 
    const allStatements = [newImport, ...source.statements, executeCyCall];
    return f.updateSourceFile(source, allStatements);
  };