1/**
2 * Copyright 2023-present 650 Industries (Expo). All rights reserved.
3 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 *
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8import { FBSourceFunctionMap, MetroSourceMapSegmentTuple } from 'metro-source-map';
9import worker, {
10  JsTransformerConfig,
11  JsTransformOptions,
12  TransformResponse,
13} from 'metro-transform-worker';
14
15import { wrapDevelopmentCSS } from './css';
16import { matchCssModule, transformCssModuleWeb } from './css-modules';
17import { transformPostCssModule } from './postcss';
18import { compileSass, matchSass } from './sass';
19
20const countLines = require('metro/src/lib/countLines') as (string: string) => number;
21
22type JSFileType = 'js/script' | 'js/module' | 'js/module/asset';
23
24type JsOutput = {
25  data: {
26    code: string;
27    lineCount: number;
28    map: MetroSourceMapSegmentTuple[];
29    functionMap: FBSourceFunctionMap | null;
30  };
31  type: JSFileType;
32};
33
34export async function transform(
35  config: JsTransformerConfig,
36  projectRoot: string,
37  filename: string,
38  data: Buffer,
39  options: JsTransformOptions
40): Promise<TransformResponse> {
41  const isCss = options.type !== 'asset' && /\.(s?css|sass)$/.test(filename);
42  // If the file is not CSS, then use the default behavior.
43  if (!isCss) {
44    const environment = options.customTransformOptions?.environment;
45
46    if (
47      environment === 'client' &&
48      // TODO: Ensure this works with windows.
49      // TODO: Add +api files.
50      filename.match(new RegExp(`^app/\\+html(\\.${options.platform})?\\.([tj]sx?|[cm]js)?$`))
51    ) {
52      // Remove the server-only +html file from the bundle when bundling for a client environment.
53      return worker.transform(
54        config,
55        projectRoot,
56        filename,
57        !options.minify
58          ? Buffer.from(
59              // Use a string so this notice is visible in the bundle if the user is
60              // looking for it.
61              '"> The server-only +html file was removed from the client JS bundle by Expo CLI."'
62            )
63          : Buffer.from(''),
64        options
65      );
66    }
67
68    return worker.transform(config, projectRoot, filename, data, options);
69  }
70
71  // If the platform is not web, then return an empty module.
72  if (options.platform !== 'web') {
73    const code = matchCssModule(filename) ? 'module.exports={ unstable_styles: {} };' : '';
74    return worker.transform(
75      config,
76      projectRoot,
77      filename,
78      // TODO: Native CSS Modules
79      Buffer.from(code),
80      options
81    );
82  }
83
84  let code = data.toString('utf8');
85
86  // Apply postcss transforms
87  code = await transformPostCssModule(projectRoot, {
88    src: code,
89    filename,
90  });
91
92  // TODO: When native has CSS support, this will need to move higher up.
93  const syntax = matchSass(filename);
94  if (syntax) {
95    code = compileSass(projectRoot, { filename, src: code }, { syntax }).src;
96  }
97
98  // If the file is a CSS Module, then transform it to a JS module
99  // in development and a static CSS file in production.
100  if (matchCssModule(filename)) {
101    const results = await transformCssModuleWeb({
102      filename,
103      src: code,
104      options: {
105        projectRoot,
106        dev: options.dev,
107        minify: options.minify,
108        sourceMap: false,
109      },
110    });
111
112    const jsModuleResults = await worker.transform(
113      config,
114      projectRoot,
115      filename,
116      Buffer.from(results.output),
117      options
118    );
119
120    const cssCode = results.css.toString();
121    const output: JsOutput[] = [
122      {
123        type: 'js/module',
124        data: {
125          // @ts-expect-error
126          ...jsModuleResults.output[0]?.data,
127
128          // Append additional css metadata for static extraction.
129          css: {
130            code: cssCode,
131            lineCount: countLines(cssCode),
132            map: [],
133            functionMap: null,
134          },
135        },
136      },
137    ];
138
139    return {
140      dependencies: jsModuleResults.dependencies,
141      output,
142    };
143  }
144
145  // Global CSS:
146
147  const { transform } = await import('lightningcss');
148
149  // TODO: Add bundling to resolve imports
150  // https://lightningcss.dev/bundling.html#bundling-order
151
152  const cssResults = transform({
153    filename,
154    code: Buffer.from(code),
155    sourceMap: false,
156    cssModules: false,
157    projectRoot,
158    minify: options.minify,
159  });
160
161  // TODO: Warnings:
162  // cssResults.warnings.forEach((warning) => {
163  // });
164
165  // Create a mock JS module that exports an empty object,
166  // this ensures Metro dependency graph is correct.
167  const jsModuleResults = await worker.transform(
168    config,
169    projectRoot,
170    filename,
171    options.dev ? Buffer.from(wrapDevelopmentCSS({ src: code, filename })) : Buffer.from(''),
172    options
173  );
174
175  const cssCode = cssResults.code.toString();
176
177  // In production, we export the CSS as a string and use a special type to prevent
178  // it from being included in the JS bundle. We'll extract the CSS like an asset later
179  // and append it to the HTML bundle.
180  const output: JsOutput[] = [
181    {
182      type: 'js/module',
183      data: {
184        // @ts-expect-error
185        ...jsModuleResults.output[0]?.data,
186
187        // Append additional css metadata for static extraction.
188        css: {
189          code: cssCode,
190          lineCount: countLines(cssCode),
191          map: [],
192          functionMap: null,
193        },
194      },
195    },
196  ];
197
198  return {
199    dependencies: jsModuleResults.dependencies,
200    output,
201  };
202}
203
204/**
205 * A custom Metro transformer that adds support for processing Expo-specific bundler features.
206 * - Global CSS files on web.
207 * - CSS Modules on web.
208 * - TODO: Tailwind CSS on web.
209 */
210module.exports = {
211  // Use defaults for everything that's not custom.
212  ...worker,
213  transform,
214};
215