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';
17
18const countLines = require('metro/src/lib/countLines') as (string: string) => number;
19
20type JSFileType = 'js/script' | 'js/module' | 'js/module/asset';
21
22type JsOutput = {
23  data: {
24    code: string;
25    lineCount: number;
26    map: MetroSourceMapSegmentTuple[];
27    functionMap: FBSourceFunctionMap | null;
28  };
29  type: JSFileType;
30};
31
32export async function transform(
33  config: JsTransformerConfig,
34  projectRoot: string,
35  filename: string,
36  data: Buffer,
37  options: JsTransformOptions
38): Promise<TransformResponse> {
39  const isCss = options.type !== 'asset' && filename.endsWith('.css');
40  // If the file is not CSS, then use the default behavior.
41  if (!isCss) {
42    return worker.transform(config, projectRoot, filename, data, options);
43  }
44
45  // If the platform is not web, then return an empty module.
46  if (options.platform !== 'web') {
47    const code = matchCssModule(filename) ? 'module.exports={};' : '';
48    return worker.transform(
49      config,
50      projectRoot,
51      filename,
52      // TODO: Native CSS Modules
53      Buffer.from(code),
54      options
55    );
56  }
57
58  const code = data.toString('utf8');
59
60  // If the file is a CSS Module, then transform it to a JS module
61  // in development and a static CSS file in production.
62  if (matchCssModule(filename)) {
63    const results = await transformCssModuleWeb({
64      filename,
65      src: code,
66      options: {
67        projectRoot,
68        dev: options.dev,
69        minify: options.minify,
70        sourceMap: false,
71      },
72    });
73
74    if (options.dev) {
75      // Dev has the CSS appended to the JS file.
76      return worker.transform(config, projectRoot, filename, Buffer.from(results.output), options);
77    }
78
79    const jsModuleResults = await worker.transform(
80      config,
81      projectRoot,
82      filename,
83      Buffer.from(results.output),
84      options
85    );
86
87    const cssCode = results.css.toString();
88    const output: JsOutput[] = [
89      {
90        type: 'js/module',
91        data: {
92          // @ts-expect-error
93          ...jsModuleResults.output[0].data,
94
95          // Append additional css metadata for static extraction.
96          css: {
97            code: cssCode,
98            lineCount: countLines(cssCode),
99            map: [],
100            functionMap: null,
101          },
102        },
103      },
104    ];
105
106    return {
107      dependencies: jsModuleResults.dependencies,
108      output,
109    };
110  }
111
112  // Global CSS:
113
114  if (options.dev) {
115    return worker.transform(
116      config,
117      projectRoot,
118      filename,
119      // In development, we use a JS file that appends a style tag to the
120      // document. This is necessary because we need to replace the style tag
121      // when the CSS changes.
122      // NOTE: We may change this to better support static rendering in the future.
123      Buffer.from(wrapDevelopmentCSS({ src: code, filename })),
124      options
125    );
126  }
127
128  const { transform } = await import('lightningcss');
129
130  // TODO: Add bundling to resolve imports
131  // https://lightningcss.dev/bundling.html#bundling-order
132
133  const cssResults = transform({
134    filename,
135    code: Buffer.from(code),
136    sourceMap: false,
137    cssModules: false,
138    projectRoot,
139    minify: options.minify,
140  });
141
142  // TODO: Warnings:
143  // cssResults.warnings.forEach((warning) => {
144  // });
145
146  // Create a mock JS module that exports an empty object,
147  // this ensures Metro dependency graph is correct.
148  const jsModuleResults = await worker.transform(
149    config,
150    projectRoot,
151    filename,
152    Buffer.from(''),
153    options
154  );
155
156  const cssCode = cssResults.code.toString();
157
158  // In production, we export the CSS as a string and use a special type to prevent
159  // it from being included in the JS bundle. We'll extract the CSS like an asset later
160  // and append it to the HTML bundle.
161  const output: JsOutput[] = [
162    {
163      data: {
164        // @ts-expect-error
165        ...jsModuleResults.output[0].data,
166
167        // Append additional css metadata for static extraction.
168        css: {
169          code: cssCode,
170          lineCount: countLines(cssCode),
171          map: [],
172          functionMap: null,
173        },
174      },
175      type: 'js/module',
176    },
177  ];
178
179  return {
180    dependencies: jsModuleResults.dependencies,
181    output,
182  };
183}
184
185/**
186 * A custom Metro transformer that adds support for processing Expo-specific bundler features.
187 * - Global CSS files on web.
188 * - CSS Modules on web.
189 * - TODO: Tailwind CSS on web.
190 */
191module.exports = {
192  // Use defaults for everything that's not custom.
193  ...worker,
194
195  transform,
196};
197