Skip to content

Commit 350af8d

Browse files
committed
Created a utility that runs esbuild and SWC.
This supports a compat profile that works as far back as safari 8, maybe earlier.
1 parent e9c090e commit 350af8d

12 files changed

+1000
-1377
lines changed

buildLib/commandLineHelper.mjs

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import process from 'node:process';
2+
3+
/**
4+
* @file This file handles parsing command line arguments for any custom build tools used in building opentype.js
5+
*/
6+
7+
export default function createCommandLineArgumentsHandler(validArguments) {
8+
const validTypes = ['json', 'string', 'number', 'boolean'];
9+
const argumentInfo = {};
10+
const shortArgsMap = {};
11+
const defaultTemplate = {};
12+
{
13+
const inputKeys = Object.keys(validArguments);
14+
for(let i = 0; i < inputKeys.length; i++) {
15+
const key = inputKeys[i];
16+
const firstCode = key[0].charCodeAt(0);
17+
if((firstCode < 65 || firstCode > 90) && (firstCode < 97 || firstCode > 122))
18+
continue;
19+
const argument = validArguments[key];
20+
if(argument.type && validTypes.includes(argument.type)){
21+
argumentInfo[key] = {};
22+
argumentInfo[key].type = argument.type;
23+
argumentInfo[key].hasShortKey = argument.shortKey??(argument.type === 'boolean');
24+
argumentInfo[key].takesParameter = (argument.type !== 'boolean'? true : argument.takesParameter??false);
25+
argumentInfo[key].acceptsPropertySyntax = argument.propertySyntax;
26+
if(argument.default){
27+
defaultTemplate[key] = argument.default;
28+
}
29+
}
30+
}
31+
validArguments = null;
32+
}
33+
const argumentKeys = Object.keys(argumentInfo);
34+
for(let i = 0; i < argumentKeys.length; i++) {
35+
const key = argumentKeys[i];
36+
if(!argumentInfo[key].hasShortKey)
37+
continue;
38+
let shortKey = null;
39+
let shortKeyCandidate = key[0].toLowerCase();
40+
for(let j = 0; j < 26; j++){
41+
if(!shortArgsMap[shortKeyCandidate]){
42+
shortArgsMap[shortKeyCandidate] = key;
43+
shortKey = shortKeyCandidate;
44+
break;
45+
}else if(!shortArgsMap[shortKeyCandidate.toUpperCase()]){
46+
shortArgsMap[shortKeyCandidate.toUpperCase()] = key;
47+
shortKey = shortKeyCandidate.toUpperCase();
48+
break;
49+
}
50+
shortKeyCandidate = String.fromCharCode(((shortKeyCandidate.charCodeAt(0)-95)%26)+96);
51+
console.log(shortKeyCandidate);
52+
}
53+
if(!shortKey)
54+
throw new Error(`Could not assign short key for argument: ${key}`);
55+
}
56+
57+
function checkForSplitValue(value, args, index){
58+
if(value[0] == "'"){
59+
return value[value.length-1] !== "'";
60+
}else if(value[0] == '"'){
61+
return value[value.length-1] !== '"';
62+
}
63+
return false;
64+
}
65+
66+
function parseBooleanArgument(key, args, index, options, value){
67+
if(value !== null && !argumentInfo[key].takesParameter){
68+
throw new Error(`Invalid option: ${key}`);
69+
}else if(value === null && !argumentInfo[key].takesParameter){
70+
options[key] = true;
71+
return 0;
72+
}else if(argumentInfo[key].takesParameter){
73+
let increment = 0;
74+
if(value === null && args.length > index+1){
75+
value = args[index+1];
76+
increment = 1;
77+
}else if(value === null){
78+
throw new Error(`Invalid option: ${key}`);
79+
}else if(checkForSplitValue(value)){
80+
do{
81+
if(args.length <= index+increment)
82+
throw new Error(`Unclosed option value: ${key}`);
83+
value += ' ' + args[index+1];
84+
increment++;
85+
}while(checkForSplitValue(value));
86+
value = value.slice(1,-1);
87+
}
88+
options[key] = value;
89+
return increment;
90+
}else{
91+
throw new Error(`Invalid option: ${key}`);
92+
}
93+
}
94+
95+
function parseNumberArgument(key, args, index, options, value){
96+
let increment = 0;
97+
if(value === null && args.length > index+1){
98+
value = args[index+1];
99+
increment = 1;
100+
}
101+
if(value === null)
102+
throw new Error(`Invalid option: ${key}`);
103+
if(checkForSplitValue(value))
104+
throw new Error(`Unclosed option value: ${key}`);
105+
if(value.startsWith("0x")){
106+
options[key] = parseInt(value.slice(2), 16);
107+
}else if(value.startsWith("0o")){
108+
options[key] = parseInt(value.slice(2), 8);
109+
}else if(value.startsWith("0b")){
110+
options[key] = parseInt(value.slice(2), 2);
111+
}else{
112+
if(value.startsWith("0d")){
113+
options[key]=parseInt(value.slice(2),10);
114+
} else options[key] = parseFloat(value);
115+
}
116+
return increment;
117+
}
118+
119+
function parseStringArgument(key, args, index, options, value){
120+
let increment = 0;
121+
if(value === null && args.length > index+1){
122+
value = args[index+1];
123+
increment = 1;
124+
}
125+
if(value === null)
126+
throw new Error(`Invalid option: ${key}`);
127+
if(checkForSplitValue(value)){
128+
do{
129+
if(args.length <= index+increment)
130+
throw new Error(`Unclosed option value: ${key}`);
131+
value += ' ' + args[index+1];
132+
increment++;
133+
}while(checkForSplitValue(value));
134+
value = value.slice(1,-1);
135+
}
136+
options[key] = value;
137+
return increment;
138+
}
139+
140+
function parseJsonArgument(key, args, index, options, value){
141+
let increment = 0;
142+
if(value === null && args.length > index+1){
143+
value = args[index+1];
144+
increment = 1;
145+
}
146+
if(value === null)
147+
throw new Error(`Invalid option: ${key}`);
148+
if(checkForSplitValue(value)){
149+
do{
150+
if(args.length <= index+increment)
151+
throw new Error(`Unclosed option value: ${key}`);
152+
value += ' ' + args[index+1];
153+
increment++;
154+
}while(checkForSplitValue(value));
155+
value = value.slice(1,-1);
156+
}
157+
options[key] = JSON.parse(value.replaceAll("'", "\""));
158+
return increment;
159+
}
160+
161+
162+
return function() {
163+
const args = process.argv.slice(2);
164+
const parsedArgs = {args: [], options: Object.assign({}, defaultTemplate)};
165+
166+
for (let i = 0; i < args.length; i++) {
167+
const arg = args[i];
168+
if (arg.startsWith('--')) {
169+
const longArg = arg.slice(2);
170+
let key = longArg;
171+
let value = null;
172+
if (!argumentInfo[key.replace(/-/g, '_')]) {
173+
let splitproperty = longArg.split("=");
174+
if (splitproperty.length > 1) {
175+
key = splitproperty[0];
176+
value = splitproperty[1];
177+
}else{
178+
throw new Error(`Invalid option: ${key}`);
179+
}
180+
if(!argumentInfo[key])
181+
throw new Error(`Invalid option: ${key}`);
182+
if(!argumentInfo[key].acceptsPropertySyntax)
183+
throw new Error(`Invalid property syntax for option: ${key}`);
184+
}
185+
if(key.indexOf('_') !== -1)
186+
throw new Error(`Invalid option: ${key}`);
187+
key = key.replace(/-/g, '_');
188+
switch(argumentInfo[key].type) {
189+
case 'boolean':
190+
i += parseBooleanArgument(key, args, i, parsedArgs.options, value);
191+
break;
192+
case 'number':
193+
i += parseNumberArgument(key, args, i, parsedArgs.options, value);
194+
break;
195+
case 'string':
196+
i += parseStringArgument(key, args, i, parsedArgs.options, value);
197+
break;
198+
case 'json':
199+
i += parseJsonArgument(key, args, i, parsedArgs.options, value);
200+
break;
201+
}
202+
} else if (arg.startsWith('-')) {
203+
const shortArg = arg.slice(1);
204+
for (let j = 0; j < shortArg.length; j++) {
205+
const key = shortArgsMap[shortArg[j]];
206+
if (!key) {
207+
throw new Error(`Invalid option: ${shortArg[j]}`);
208+
}
209+
if(argumentInfo[key].type === 'boolean'){
210+
i += parseBooleanArgument(key, args, i, parsedArgs.options, null);
211+
}else if(j > 0 || shortArg.length > 1){
212+
throw new Error(`Invalid option: ${shortArg}`);
213+
}else{
214+
switch(argumentInfo[key].type) {
215+
case 'number':
216+
i += parseNumberArgument(key, args, i, parsedArgs.options, null);
217+
break;
218+
case 'string':
219+
i += parseStringArgument(key, args, i, parsedArgs.options, null);
220+
break;
221+
case 'json':
222+
i += parseJsonArgument(key, args, i, parsedArgs.options, null);
223+
break;
224+
}
225+
}
226+
}
227+
}else{
228+
parsedArgs.args.push(arg);
229+
}
230+
}
231+
232+
return parsedArgs;
233+
}
234+
};

buildLib/esbuild-plugin-swc.mjs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
This file's license:
3+
4+
MIT License
5+
6+
Copyright (c) 2021 sanyuan et al.
7+
8+
Permission is hereby granted, free of charge, to any person obtaining a copy
9+
of this software and associated documentation files (the "Software"), to deal
10+
in the Software without restriction, including without limitation the rights
11+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
copies of the Software, and to permit persons to whom the Software is
13+
furnished to do so, subject to the following conditions:
14+
15+
The above copyright notice and this permission notice shall be included in all
16+
copies or substantial portions of the Software.
17+
18+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
SOFTWARE.
25+
*/
26+
27+
/**
28+
* @file This file contains the esbuild plugin for SWC
29+
*/
30+
31+
import { transform, transformSync } from '@swc/core';
32+
import path from 'node:path';
33+
import process from 'node:process';
34+
import fs from 'node:fs/promises';
35+
36+
function assignDeep(target, source) {
37+
for (const key in source) {
38+
if (source[key] instanceof Object && !(source[key] instanceof Function)) {
39+
if (!target[key]) {
40+
target[key] = {};
41+
}
42+
assignDeep(target[key], source[key]);
43+
} else {
44+
target[key] = source[key];
45+
}
46+
}
47+
return target;
48+
}
49+
50+
export default function swcPlugin(options, isAsync) {
51+
options = options ?? {};
52+
isAsync = isAsync ?? true;
53+
return {
54+
name: 'esbuild:swc',
55+
setup(builder) {
56+
builder.onResolve({ filter: /\.(m?[tj]s)$/ }, (args) => {
57+
const fullPath = path.resolve(args.resolveDir, args.path);
58+
return {
59+
path: fullPath,
60+
};
61+
});
62+
builder.onLoad({ filter: /\.(m?[tj]s)$/ }, async (args) => {
63+
const code = await fs.readFile(args.path, 'utf-8');
64+
const isTS = args.path.endsWith('.ts');
65+
const initialOptions = {
66+
jsc: {
67+
parser: {
68+
syntax: isTS ? 'typescript' : 'ecmascript',
69+
}
70+
},
71+
filename: args.path,
72+
sourceMaps: true,
73+
sourceFileName: path.relative(options.root,args.path)
74+
};
75+
const finalOptions = assignDeep(assignDeep({}, initialOptions), options);
76+
let result;
77+
if (isAsync) {
78+
result = await transform(code, finalOptions);
79+
}else{
80+
result = transformSync(code, finalOptions);
81+
}
82+
return {
83+
contents: result.code+(finalOptions.sourceMaps?`\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(result.map).toString('base64')}`:''),
84+
loader: 'js'
85+
};
86+
});
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)