Skip to content

Commit 430970a

Browse files
authored
Merge pull request #499 from antfu/feat/fetch-reader
Universal reader
2 parents 02d9898 + 551a883 commit 430970a

File tree

10 files changed

+287
-67
lines changed

10 files changed

+287
-67
lines changed

package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,8 @@
6868
"webpack": "^4.41.4",
6969
"webpack-cli": "^3.3.10",
7070
"webpack-shell-plugin": "^0.5.0"
71+
},
72+
"dependencies": {
73+
"xmlhttprequest": "^1.8.0"
7174
}
7275
}

src/browser_runtime.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,26 @@
99
script.attributes.outputHanzi &&
1010
script.attributes.outputHanzi.value === "false"
1111
);
12+
const allowHttp = !!script.attributes.allowHttp;
1213
let code = script.innerText;
14+
let importPaths = [window.location.origin];
1315
if (script.src) {
1416
const response = await fetch(script.src);
1517
code = (await response.text()).toString();
18+
importPaths.push(
19+
script.src
20+
.split("/")
21+
.slice(0, -1)
22+
.join("/")
23+
);
1624
}
1725
execute(code, {
1826
scoped,
1927
outputHanzi,
2028
logCallback: isDev ? console.log : () => {},
21-
resetVarCnt: false
29+
resetVarCnt: false,
30+
importPaths,
31+
allowHttp
2232
});
2333
}
2434

src/cli.js

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ program
3838
"--roman [method]",
3939
'Romanize identifiers. The method can be "pinyin", "baxter" or "unicode"'
4040
)
41+
.option("--strict", "Enable static typechecking")
42+
.option("--allowHttp", "Allow to import from http")
43+
.option("--dir <path>", "Directory to importing from, seprates with comma(,)")
4144
.option("--outputHanzi", "Convert output to hanzi", true)
4245
.option("--log <file>", "Save log to file")
4346
.option("--title <title>", "Override title in rendering")
@@ -61,17 +64,21 @@ if (emptyCall || showHelp) {
6164

6265
program.parse(process.argv);
6366

64-
preprocess();
67+
(async () => {
68+
preprocess();
6569

66-
if (program.compile) {
67-
output(getCompiled());
68-
} else if (program.render) {
69-
doRender();
70-
} else if (program.interactive) {
71-
intreactive();
72-
} else {
73-
exec();
74-
}
70+
if (program.compile) {
71+
output(await getCompiled());
72+
} else if (program.render) {
73+
doRender();
74+
} else if (program.interactive) {
75+
await intreactive();
76+
} else {
77+
await exec();
78+
}
79+
})().catch(e => {
80+
console.error(e);
81+
});
7582

7683
// ====== Utils ======
7784

@@ -103,14 +110,35 @@ function preprocess() {
103110

104111
function getCompiled() {
105112
const source = getSource();
106-
return compile(program.lang, source, {
113+
114+
return compile(source, {
115+
...getCompileOptions()
116+
});
117+
}
118+
119+
function getImportPaths() {
120+
const pathes = [];
121+
if (program.dir) {
122+
pathes.push(...program.dir.split(","));
123+
}
124+
pathes.push(...program.files.map(file => path.resolve(path.dirname(file))));
125+
pathes.push(path.resolve("."));
126+
return Array.from(new Set(pathes));
127+
}
128+
129+
function getCompileOptions() {
130+
return {
131+
lang: program.lang,
107132
romanizeIdentifiers: program.roman,
133+
strict: !!program.strict,
134+
allowHttp: !!program.allowHttp,
135+
importPaths: getImportPaths(),
108136
logCallback: logHandler(program.log, "a"),
109137
errorCallback: function(x) {
110138
console.error(x);
111139
process.exit();
112140
}
113-
});
141+
};
114142
}
115143

116144
function resolvePath(x) {
@@ -125,6 +153,7 @@ function getSource() {
125153
: fs.readFileSync(resolvePath(x)).toString()
126154
)
127155
.join("\n");
156+
128157
if (program.eval) scripts += `\n${program.eval}`;
129158

130159
return scripts;
@@ -176,25 +205,25 @@ function doRender() {
176205
}
177206
}
178207

179-
function intreactive() {
208+
async function intreactive() {
180209
if (program.lang !== "js") {
181210
console.error(
182211
`Target language "${program.lang}" is not supported for intreactive mode.`
183212
);
184213
process.exit(1);
185214
}
186215
replscope();
187-
repl(getCompiled());
216+
repl(await getCompiled());
188217
}
189-
function exec() {
218+
async function exec() {
190219
if (program.lang !== "js") {
191220
console.error(
192221
`Target language "${program.lang}" is not supported for direct executing. Please use --compile option instead.`
193222
);
194223
process.exit(1);
195224
}
196225

197-
evalCompiled(getCompiled(), {
226+
evalCompiled(await getCompiled(), {
198227
outputHanzi: program.outputHanzi,
199228
lang: program.lang
200229
});

src/macro.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
function extractMacros(txt, { lib, reader, lang }) {
1+
function extractMacros(txt, options = {}) {
2+
const { lib, reader, lang, importPaths, requestOptions } = options;
3+
24
function getImports() {
35
var imps = [];
46
var qlvl = 0;
@@ -119,9 +121,9 @@ function extractMacros(txt, { lib, reader, lang }) {
119121
} else if (imports[i] in lib) {
120122
isrc = lib[imports[i]];
121123
} else {
122-
isrc = reader(imports[i]);
124+
isrc = reader(imports[i], importPaths, requestOptions);
123125
}
124-
macros = macros.concat(extractMacros(isrc, { lib, reader, lang }));
126+
macros = macros.concat(extractMacros(isrc, options));
125127
}
126128
return macros;
127129
}

src/parser.js

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ var version = require("./version");
66
var compilers = require("./compiler/compilers");
77
var { typecheck, printSignature } = require("./typecheck");
88
var { expandMacros, extractMacros } = require("./macro.js");
9+
var { defaultImportReader } = require("./reader");
10+
11+
const defaultTrustedHosts = [
12+
"https://raw.githubusercontent.com/LingDong-/wenyan-lang/master"
13+
];
914

1015
function wy2tokens(
1116
txt,
@@ -213,6 +218,12 @@ function tokenRomanize(tokens, method) {
213218
}
214219
}
215220

221+
function defaultLogCallback(x) {
222+
return typeof x == "string"
223+
? console.log(x)
224+
: console.dir(x, { depth: null, maxArrayLength: null });
225+
}
226+
216227
function tokens2asc(
217228
tokens,
218229
assert = (msg, pos, b) => {
@@ -624,29 +635,6 @@ function pyWrapModule(name, src) {
624635
return `#/*___wenyan_module_${name}_start___*/\n${src}\n#/*___wenyan_module_${name}_end___*/\n`;
625636
}
626637

627-
function defaultReader(x) {
628-
try {
629-
const fs = eval("require")("fs");
630-
try {
631-
return fs.readFileSync(x + ".wy").toString();
632-
} catch (e) {
633-
var files = fs.readdirSync("./");
634-
for (var i = 0; i < files.length; i++) {
635-
if (fs.lstatSync(files[i]).isDirectory()) {
636-
try {
637-
return fs.readFileSync(files[i] + "/" + x + ".wy").toString();
638-
} catch (e) {}
639-
}
640-
}
641-
}
642-
console.log("Cannot import ", x);
643-
} catch (e) {
644-
console.error(
645-
`Cannot import ${x}, please specify the "reader" option in compile.`
646-
);
647-
}
648-
}
649-
650638
function compile(arg1, arg2, arg3) {
651639
let options = {};
652640
let txt = "";
@@ -663,17 +651,26 @@ function compile(arg1, arg2, arg3) {
663651
const {
664652
lang = "js",
665653
romanizeIdentifiers = "none",
666-
resetVarCnt,
667-
logCallback = x =>
668-
typeof x == "string"
669-
? console.log(x)
670-
: console.dir(x, { depth: null, maxArrayLength: null }),
654+
resetVarCnt = true,
655+
logCallback = defaultLogCallback,
671656
errorCallback = process.exit,
672657
lib = typeof STDLIB == "undefined" ? {} : STDLIB,
673-
reader = defaultReader,
674-
strict = false
658+
reader = defaultImportReader,
659+
importPaths = [],
660+
strict = false,
661+
allowHttp = false,
662+
trustedHosts = [],
663+
requestTimeout = 2000
675664
} = options;
676665

666+
trustedHosts.push(...defaultTrustedHosts);
667+
668+
const requestOptions = {
669+
allowHttp,
670+
trustedHosts,
671+
requestTimeout
672+
};
673+
677674
if (resetVarCnt) idenMap = {};
678675
txt = (txt || "").replace(/\r\n/g, "\n");
679676

@@ -703,7 +700,13 @@ function compile(arg1, arg2, arg3) {
703700
return 0;
704701
}
705702

706-
var macros = extractMacros(txt, { lib, reader, lang });
703+
var macros = extractMacros(txt, {
704+
lib,
705+
reader,
706+
lang,
707+
importPaths,
708+
requestOptions
709+
});
707710
txt = expandMacros(txt, macros);
708711

709712
logCallback("\n\n=== [PASS 0] EXPAND-MACROS ===");
@@ -751,19 +754,15 @@ function compile(arg1, arg2, arg3) {
751754
} else if (imports[i] in lib) {
752755
isrc = lib[imports[i]];
753756
} else {
754-
isrc = reader(imports[i]);
757+
isrc = reader(imports[i], importPaths, requestOptions);
755758
}
756759
targ =
757760
mwrapper(
758761
imports[i],
759762
compile(isrc, {
760-
lang,
761-
romanizeIdentifiers,
763+
...options,
762764
resetVarCnt: false,
763-
strict: false,
764-
logCallback,
765-
errorCallback,
766-
lib
765+
strict: false
767766
})
768767
) + targ;
769768
}

src/reader.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
function isHostTrusted(url, trustedHosts) {
2+
for (const host of trustedHosts) {
3+
// FIXME: it can be bypassed by relative path resolving,
4+
// for examples: https://trusted.com/a/../../hijack.com/a/
5+
if (url.startsWith(host)) return true;
6+
}
7+
return false;
8+
}
9+
10+
function isHttpURL(uri) {
11+
return !!uri.match(/^https?\:\/\//);
12+
}
13+
14+
function fetchTextSync(url, timeout) {
15+
let XHR;
16+
if (typeof window !== "undefined" && "XMLHttpRequest" in window)
17+
XHR = window.XMLHttpRequest;
18+
else XHR = eval("require")("xmlhttprequest").XMLHttpRequest;
19+
20+
var xmlHttp = new XHR();
21+
xmlHttp.timeout = timeout;
22+
xmlHttp.open("GET", url, false); // false for synchronous request
23+
xmlHttp.send(null);
24+
25+
if (xmlHttp.status >= 200 && xmlHttp.status < 300)
26+
return xmlHttp.responseText;
27+
28+
throw new URIError(xmlHttp.responseText);
29+
}
30+
31+
function defaultImportReader(
32+
moduleName,
33+
importPaths = [],
34+
requestOptions = {}
35+
) {
36+
const {
37+
allowHttp = false,
38+
trustedHosts = [],
39+
requestTimeout = 2000
40+
} = requestOptions;
41+
42+
if (typeof importPaths === "string") importPaths = [importPaths];
43+
44+
for (dir of importPaths) {
45+
const uri = dir + "/" + moduleName + ".wy";
46+
if (isHttpURL(uri)) {
47+
if (!allowHttp && !isHostTrusted(uri, trustedHosts)) {
48+
throw new URIError(
49+
`URL request "${uri}" is blocked by default for security purpose. ` +
50+
`You can turn it on by specify the "allowHttp" option.`
51+
);
52+
}
53+
54+
try {
55+
return fetchTextSync(uri, requestTimeout);
56+
} catch (e) {}
57+
} else {
58+
try {
59+
return eval("require")("fs").readFileSync(uri, "utf-8");
60+
} catch (e) {}
61+
}
62+
}
63+
64+
throw new ReferenceError(
65+
`Module "${moduleName}" is not found. Searched in ${importPaths}`
66+
);
67+
}
68+
69+
module.exports = {
70+
defaultImportReader
71+
};

0 commit comments

Comments
 (0)