/
index.ts
150 lines (125 loc) · 4.17 KB
/
index.ts
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
148
149
150
/**
* This file contains an esbuild loader for Linaria.
* It uses the transform.ts function to generate class names from source code,
* returns transformed code without template literals and attaches generated source maps
*/
import fs from 'fs';
import path from 'path';
import type { Plugin, TransformOptions, Loader } from 'esbuild';
import { transformSync } from 'esbuild';
import type { PluginOptions, Preprocessor } from '@linaria/babel-preset';
import { slugify, transform } from '@linaria/babel-preset';
type EsbuildPluginOptions = {
sourceMap?: boolean;
preprocessor?: Preprocessor;
esbuildOptions?: TransformOptions;
} & Partial<PluginOptions>;
const nodeModulesRegex = /^(?:.*[\\/])?node_modules(?:[\\/].*)?$/;
export default function linaria({
sourceMap,
preprocessor,
esbuildOptions,
...rest
}: EsbuildPluginOptions = {}): Plugin {
let options = esbuildOptions;
return {
name: 'linaria',
setup(build) {
const cssLookup = new Map<string, string>();
const asyncResolve = async (
token: string,
importer: string
): Promise<string> => {
const context = path.isAbsolute(importer)
? path.dirname(importer)
: path.join(process.cwd(), path.dirname(importer));
const result = await build.resolve(token, {
resolveDir: context,
kind: 'import-statement',
});
if (result.errors.length > 0) {
throw new Error(`Cannot resolve ${token}`);
}
return result.path;
};
build.onResolve({ filter: /\.linaria\.css$/ }, (args) => {
return {
namespace: 'linaria',
path: args.path,
};
});
build.onLoad({ filter: /.*/, namespace: 'linaria' }, (args) => {
return {
contents: cssLookup.get(args.path),
loader: 'css',
resolveDir: path.basename(args.path),
};
});
build.onLoad({ filter: /\.(js|jsx|ts|tsx)$/ }, async (args) => {
const rawCode = fs.readFileSync(args.path, 'utf8');
const { ext, name: filename } = path.parse(args.path);
const loader = ext.replace(/^\./, '') as Loader;
if (nodeModulesRegex.test(args.path)) {
return {
loader,
contents: rawCode,
};
}
if (!options) {
options = {};
if ('jsxFactory' in build.initialOptions) {
options.jsxFactory = build.initialOptions.jsxFactory;
}
if ('jsxFragment' in build.initialOptions) {
options.jsxFragment = build.initialOptions.jsxFragment;
}
}
const transformed = transformSync(rawCode, {
...options,
sourcefile: args.path,
sourcemap: sourceMap,
loader,
});
let { code } = transformed;
if (sourceMap) {
const esbuildMap = Buffer.from(transformed.map).toString('base64');
code += `/*# sourceMappingURL=data:application/json;base64,${esbuildMap}*/`;
}
const result = await transform(
code,
{
filename: args.path,
preprocessor,
pluginOptions: rest,
},
asyncResolve
);
if (!result.cssText) {
return {
contents: code,
loader,
resolveDir: path.dirname(args.path),
};
}
let { cssText } = result;
const slug = slugify(cssText);
const cssFilename = `${filename}_${slug}.linaria.css`;
let contents = `import ${JSON.stringify(cssFilename)}; ${result.code}`;
if (sourceMap && result.cssSourceMapText) {
const map = Buffer.from(result.cssSourceMapText).toString('base64');
cssText += `/*# sourceMappingURL=data:application/json;base64,${map}*/`;
const linariaMap = Buffer.from(
JSON.stringify(result.sourceMap)
).toString('base64');
contents += `/*# sourceMappingURL=data:application/json;base64,${linariaMap}*/`;
}
cssLookup.set(cssFilename, cssText);
return {
contents,
loader,
resolveDir: path.dirname(args.path),
};
});
},
};
}