├── .editorconfig
├── .gitignore
├── .npmignore
├── README.md
├── dist
├── bundle.js
├── compiler.js
├── fetch.js
├── main.js
└── transform.js
├── example
├── dynamic_import.ts
├── equal.ts
├── example.ts
├── hoge.ts
├── import_meta.ts
├── nodelib.ts
├── other.ts
├── react.tsx
├── server.ts
└── some.ts
├── package.json
├── src
├── __tests__
│ ├── bundle.test.ts
│ ├── fetch.test.ts
│ └── transform.test.ts
├── bundle.ts
├── fetch.ts
├── main.ts
└── transform.ts
├── template
└── template.ts
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | indent_size = 2
3 |
4 | [Makefile]
5 | indent_style = tab
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .idea
3 | tmp
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | tmp
2 | .idea
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://badge.fury.io/js/%40keroxp%2Ftsb)
2 |
3 | tsb
4 | ===
5 | TypeScript/JavaScript module bundler for ESModule
6 |
7 | ## Description
8 |
9 | `tsb` is module bundler for ECMAScript. It bundles TypeScript/JavaScript modules built with pure ESModule.
10 |
11 | ## Concept
12 |
13 | - **TypeScript first**
14 | - tsb bundles and transpiles ts/js files with TypeScript Compiler API
15 | - **ESM only**
16 | - tsb only supports ECMAScript that are written with pure ESModule (import/export)
17 | - CommonJS,AMD (require/exports) are **NOT supported**
18 | - **URL import support**
19 | - tsb will automatically fetch URL import/export and bundles all dependencies and stores caches.
20 |
21 | ## Install
22 | Via yarn
23 |
24 | ```bash
25 | $ yarn global add @keroxp/tsb
26 | ```
27 |
28 | Via npm
29 |
30 | ```bash
31 | $ npm i -g @keroxp/tsb
32 | ```
33 |
34 | or
35 |
36 | ```bash
37 | $ npx @keroxp/tsb
38 | ```
39 | ## Usage
40 |
41 | ```bash
42 | $ tsb ./example/server.ts > bundle.js
43 | ```
44 |
45 | ## License
46 |
47 | MIT
--------------------------------------------------------------------------------
/dist/bundle.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | // Copyright 2019 Yusuke Sakurai. All rights reserved. MIT license.
4 | const path = require("path");
5 | const url = require("url");
6 | const transform_1 = require("./transform");
7 | const fs = require("fs-extra");
8 | const ts = require("typescript");
9 | const fetch_1 = require("./fetch");
10 | exports.kUriRegex = /^(https?):\/\/(.+?)$/;
11 | exports.kRelativeRegex = /^\.\.?\/.+?\.[jt]sx?$/;
12 | async function readFileAsync(file) {
13 | return String(await fs.readFile(file));
14 | }
15 | async function fileExists(p) {
16 | return fs.pathExists(p);
17 | }
18 | async function resolveUri(id) {
19 | if (id.match(exports.kUriRegex)) {
20 | return fetch_1.urlToCacheFilePath(id);
21 | }
22 | else if (id.match(exports.kRelativeRegex)) {
23 | return path.resolve(id);
24 | }
25 | else {
26 | throw new Error("invalid module specifier: " + id);
27 | }
28 | }
29 | async function resolveModuleId(source, skipFetch = false) {
30 | if (source.dependency.match(exports.kUriRegex)) {
31 | // any + url
32 | const cachePath = fetch_1.urlToCacheFilePath(source.dependency);
33 | const cacheMetaPath = fetch_1.urlToCacheMetaFilePath(source.dependency);
34 | if (!(await fileExists(cachePath))) {
35 | if (!(await fileExists(cacheMetaPath))) {
36 | if (!skipFetch) {
37 | await fetch_1.fetchModule(source.dependency);
38 | return resolveModuleId(source, false);
39 | }
40 | else {
41 | throw createError(source, `
42 | Cache file was not found in: ${cachePath}.
43 | `);
44 | }
45 | }
46 | const headers = await readFileAsync(cacheMetaPath);
47 | const meta = JSON.parse(headers);
48 | if (!meta.redirectTo) {
49 | throw new Error(`meta file for ${source.dependency} may be broken`);
50 | }
51 | return resolveModuleId({
52 | moduleId: ".",
53 | dependency: meta.redirectTo
54 | });
55 | }
56 | else {
57 | return source.dependency;
58 | }
59 | }
60 | else if (source.moduleId.match(exports.kUriRegex)) {
61 | // url + relative
62 | return resolveModuleId({
63 | moduleId: ".",
64 | dependency: url.resolve(source.moduleId, source.dependency)
65 | });
66 | }
67 | else {
68 | // relative + relative
69 | return joinModuleId(source);
70 | }
71 | }
72 | exports.resolveModuleId = resolveModuleId;
73 | async function traverseDependencyTree(sourceFile, dependencyTree, redirectionMap, opts) {
74 | const dependencies = [];
75 | let id;
76 | id = await resolveModuleId(sourceFile, opts.skipFetch);
77 | redirectionMap.set(joinModuleId(sourceFile), id);
78 | if (dependencyTree.has(id)) {
79 | return;
80 | }
81 | dependencyTree.set(id, sourceFile);
82 | const visit = (node) => {
83 | if (ts.isImportDeclaration(node)) {
84 | const dependency = node.moduleSpecifier.text;
85 | dependencies.push(dependency);
86 | }
87 | else if (ts.isCallExpression(node) &&
88 | node.expression.kind === ts.SyntaxKind.ImportKeyword) {
89 | // import("aa").then(v => {})
90 | const [module] = node.arguments;
91 | if (ts.isStringLiteral(module)) {
92 | dependencies.push(module.text);
93 | }
94 | }
95 | else if (ts.isExportDeclaration(node)) {
96 | const exportClause = node.exportClause;
97 | const module = node.moduleSpecifier;
98 | if (exportClause) {
99 | if (module) {
100 | // export {a,b} form "bb"
101 | dependencies.push(module.text);
102 | }
103 | else {
104 | // export {a,b
105 | }
106 | }
107 | else {
108 | dependencies.push(module.text);
109 | }
110 | }
111 | ts.forEachChild(node, visit);
112 | };
113 | const resolvedPath = await resolveUri(id);
114 | const text = await readFileAsync(resolvedPath);
115 | const src = ts.createSourceFile(resolvedPath, text, ts.ScriptTarget.ESNext);
116 | ts.forEachChild(src, visit);
117 | for (const dependency of dependencies) {
118 | await traverseDependencyTree({ dependency: dependency, moduleId: id }, dependencyTree, redirectionMap, opts);
119 | }
120 | }
121 | function joinModuleId(source) {
122 | if (source.dependency.match(exports.kUriRegex)) {
123 | // url
124 | return source.dependency;
125 | }
126 | else if (source.moduleId.match(exports.kUriRegex)) {
127 | // url + relative
128 | return url.resolve(source.moduleId, source.dependency);
129 | }
130 | else if (source.dependency.match(exports.kRelativeRegex)) {
131 | // relative + relative
132 | const cwd = process.cwd();
133 | const dir = path.dirname(source.moduleId);
134 | return "./" + path.relative(cwd, path.join(dir, source.dependency));
135 | }
136 | else {
137 | throw createError(source, `dependency must be URL or start with ./ or ../`);
138 | }
139 | }
140 | exports.joinModuleId = joinModuleId;
141 | function createError(source, message) {
142 | return new Error(`moduleId: "${source.moduleId}", dependency: "${source.dependency}": ${message}`);
143 | }
144 | async function bundle(entry, opts) {
145 | const tree = new Map();
146 | const redirectionMap = new Map();
147 | let canonicalName;
148 | if (entry.match(exports.kUriRegex)) {
149 | canonicalName = entry;
150 | }
151 | else {
152 | canonicalName = "./" + path.relative(process.cwd(), entry);
153 | }
154 | await traverseDependencyTree({
155 | dependency: canonicalName,
156 | moduleId: "."
157 | }, tree, redirectionMap, opts);
158 | const printer = ts.createPrinter();
159 | let template = await readFileAsync(path.resolve(__dirname, "../template/template.ts"));
160 | const resolveModule = (moduleId, dep) => {
161 | const redirection = redirectionMap.get(moduleId);
162 | if (!redirection) {
163 | throw new Error(`${moduleId} not found in redirection map`);
164 | }
165 | if (dep.match(exports.kUriRegex)) {
166 | const ret = redirectionMap.get(dep);
167 | if (!ret) {
168 | throw new Error(`${dep} not found in redirection map`);
169 | }
170 | return ret;
171 | }
172 | else {
173 | return joinModuleId({
174 | moduleId: moduleId,
175 | dependency: dep
176 | });
177 | }
178 | };
179 | const modules = [];
180 | for (const [moduleId] of tree.entries()) {
181 | const transformer = new transform_1.Transformer(moduleId, resolveModule);
182 | let text = await readFileAsync(await resolveUri(moduleId));
183 | if (text.startsWith("#!")) {
184 | // disable shell
185 | text = "//" + text;
186 | }
187 | const src = ts.createSourceFile(moduleId, text, ts.ScriptTarget.ESNext);
188 | const result = ts.transform(src, transformer.transformers());
189 | const transformed = printer.printFile(result
190 | .transformed[0]);
191 | const opts = {
192 | target: ts.ScriptTarget.ESNext
193 | };
194 | if (moduleId.endsWith(".tsx") || moduleId.endsWith(".jsx")) {
195 | opts.jsx = ts.JsxEmit.React;
196 | }
197 | let body = ts.transpile(transformed, opts);
198 | if (transformer.shouldMergeExport) {
199 | body = `
200 | function __export(m,k) {
201 | if (k) {
202 | for (const p in k) if (!tsb.exports.hasOwnProperty(k[p])) tsb.exports[k[p]] = m[p];
203 | } else {
204 | for (const p in m) if (!tsb.exports.hasOwnProperty(p)) tsb.exports[p] = m[p];
205 | }
206 | }
207 | ${body}
208 | `;
209 | }
210 | modules.push(`"${moduleId}": function (tsb) { ${body} } `);
211 | }
212 | const body = modules.join(",");
213 | const entryId = await resolveModuleId({
214 | dependency: canonicalName,
215 | moduleId: "."
216 | });
217 | template = `(${template}).call(this, {${body}}, "${entryId}")`;
218 | const output = ts.transpile(template, {
219 | target: ts.ScriptTarget.ESNext
220 | });
221 | console.log(output);
222 | }
223 | exports.bundle = bundle;
224 |
--------------------------------------------------------------------------------
/dist/compiler.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | const ts = require("typescript");
4 | const path = require("path");
5 | const bundle_1 = require("./bundle");
6 | function createCompilerHost(options, moduleSearchLocations, urlResolver) {
7 | return {
8 | getSourceFile,
9 | getDefaultLibFileName: () => "lib.d.ts",
10 | writeFile: (fileName, content) => ts.sys.writeFile(fileName, content),
11 | getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
12 | getDirectories: path => ts.sys.getDirectories(path),
13 | getCanonicalFileName: fileName => ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
14 | getNewLine: () => ts.sys.newLine,
15 | useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
16 | fileExists,
17 | readFile,
18 | resolveModuleNames
19 | };
20 | function fileExists(fileName) {
21 | return ts.sys.fileExists(fileName);
22 | }
23 | function readFile(fileName) {
24 | return ts.sys.readFile(fileName);
25 | }
26 | function getSourceFile(fileName, languageVersion, onError) {
27 | const sourceText = ts.sys.readFile(fileName);
28 | return sourceText !== undefined
29 | ? ts.createSourceFile(fileName, sourceText, languageVersion)
30 | : undefined;
31 | }
32 | function resolveModuleNames(moduleNames, containingFile) {
33 | const resolvedModules = [];
34 | for (const moduleName of moduleNames) {
35 | if (moduleName.match(bundle_1.kUriRegex)) {
36 | resolvedModules.push({ resolvedFileName: urlResolver(moduleName) });
37 | }
38 | else {
39 | // try to use standard resolution
40 | let result = ts.resolveModuleName(moduleName, containingFile, options, {
41 | fileExists,
42 | readFile
43 | });
44 | if (result.resolvedModule) {
45 | resolvedModules.push(result.resolvedModule);
46 | }
47 | else {
48 | // check fallback locations, for simplicity assume that module at location
49 | // should be represented by '.d.ts' file
50 | for (const location of moduleSearchLocations) {
51 | const modulePath = path.join(location, moduleName + ".d.ts");
52 | if (fileExists(modulePath)) {
53 | resolvedModules.push({ resolvedFileName: modulePath });
54 | }
55 | }
56 | }
57 | }
58 | }
59 | return resolvedModules;
60 | }
61 | }
62 | exports.createCompilerHost = createCompilerHost;
63 | function compile(sourceFiles, moduleSearchLocations, urlResolver) {
64 | const options = {
65 | module: ts.ModuleKind.AMD,
66 | target: ts.ScriptTarget.ES5
67 | };
68 | const host = createCompilerHost(options, moduleSearchLocations, urlResolver);
69 | const program = ts.createProgram(sourceFiles, options, host);
70 | /// do something with program...
71 | }
72 |
--------------------------------------------------------------------------------
/dist/fetch.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | const node_fetch_1 = require("node-fetch");
4 | const url_1 = require("url");
5 | const cachdir = require("cachedir");
6 | const path = require("path");
7 | const fs = require("fs-extra");
8 | const colors_1 = require("colors");
9 | const crypto = require("crypto");
10 | const cacheDirectory = cachdir("tsb");
11 | function urlToCacheFilePath(url) {
12 | const u = new url_1.URL(url);
13 | if (!u.protocol.match(/^https?/)) {
14 | throw new Error("url must start with https?:" + url);
15 | }
16 | const fullPath = u.pathname + u.search;
17 | const scheme = u.protocol.startsWith("https") ? "https" : "http";
18 | const sha256 = crypto.createHash("sha256");
19 | sha256.update(fullPath);
20 | const fullPathHash = sha256.digest("hex");
21 | // ~/Library/Caches/tsb/https/deno.land/{sha256hashOfUrl}
22 | return path.join(cacheDirectory, scheme, u.host, fullPathHash);
23 | }
24 | exports.urlToCacheFilePath = urlToCacheFilePath;
25 | function urlToCacheMetaFilePath(url) {
26 | // ~/Library/Caches/tsb/https/deno.land/{sha256hashOfUrl}.meta.json
27 | return urlToCacheFilePath(url) + ".meta.json";
28 | }
29 | exports.urlToCacheMetaFilePath = urlToCacheMetaFilePath;
30 | async function saveMetaFile(url, meta) {
31 | const dest = urlToCacheMetaFilePath(url);
32 | await fs.writeFile(dest, JSON.stringify(meta));
33 | }
34 | const kAcceptableMimeTypes = [
35 | "text/plain",
36 | "application/javascript",
37 | "text/javascript",
38 | "application/typescript",
39 | "text/typescript"
40 | ];
41 | async function fetchModule(url) {
42 | const u = new url_1.URL(url);
43 | const originalPath = u.pathname + u.search;
44 | const dest = urlToCacheFilePath(url);
45 | console.error(`${colors_1.green("Download")} ${url}`);
46 | const resp = await node_fetch_1.default(url, {
47 | method: "GET",
48 | redirect: "manual"
49 | });
50 | const dir = path.dirname(dest);
51 | if (!(await fs.pathExists(dir))) {
52 | await fs.ensureDir(dir);
53 | }
54 | if (400 <= resp.status) {
55 | throw new Error(`fetch failed with status code ${resp.status}`);
56 | }
57 | if (200 <= resp.status && resp.status < 300) {
58 | const contentType = resp.headers.get("content-type") || "";
59 | if (!kAcceptableMimeTypes.some(v => contentType.startsWith(v))) {
60 | throw new Error(`unacceptable content-type for ${url}: ${contentType} `);
61 | }
62 | await Promise.all([
63 | // TODO: pipe body stream
64 | fs.writeFile(dest, await resp.text()),
65 | saveMetaFile(url, { mimeType: contentType, originalPath })
66 | ]);
67 | }
68 | else if (300 <= resp.status) {
69 | const redirectTo = resp.headers.get("location");
70 | if (!redirectTo) {
71 | throw new Error("redirected response didn't has Location headers!");
72 | }
73 | await saveMetaFile(url, {
74 | redirectTo,
75 | originalPath
76 | });
77 | return fetchModule(redirectTo);
78 | }
79 | }
80 | exports.fetchModule = fetchModule;
81 |
--------------------------------------------------------------------------------
/dist/main.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | "use strict";
3 | Object.defineProperty(exports, "__esModule", { value: true });
4 | // Copyright 2019 Yusuke Sakurai. All rights reserved. MIT license.
5 | const caporal = require("caporal");
6 | const bundle_1 = require("./bundle");
7 | caporal
8 | .name("tsb")
9 | .version("0.7.1")
10 | .argument("file", "entry file path for bundle")
11 | .option("--skipFetch", "skip fetching remote module recursively")
12 | .action(action);
13 | async function action(args, opts) {
14 | try {
15 | await bundle_1.bundle(args.file, opts);
16 | }
17 | catch (e) {
18 | if (e instanceof Error) {
19 | console.error(e.stack);
20 | }
21 | }
22 | }
23 | if (require.main) {
24 | caporal.parse(process.argv);
25 | }
26 |
--------------------------------------------------------------------------------
/dist/transform.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | // Copyright 2019 Yusuke Sakurai. All rights reserved. MIT license.
4 | const ts = require("typescript");
5 | function createTsbImportAccess() {
6 | return ts.createPropertyAccess(ts.createIdentifier("tsb"), "import");
7 | }
8 | function createTsbImportDynamicAccess() {
9 | return ts.createPropertyAccess(ts.createIdentifier("tsb"), "importDynamic");
10 | }
11 | function createTsbExportAccess() {
12 | return ts.createPropertyAccess(ts.createIdentifier("tsb"), "exports");
13 | }
14 | function createTsbResolveCall(module, dep) {
15 | return ts.createCall(ts.createPropertyAccess(ts.createIdentifier("tsb"), "resolveModule"), undefined, [ts.createStringLiteral(module), dep]);
16 | }
17 | class Transformer {
18 | constructor(moduleId, moduleResolver) {
19 | this.moduleId = moduleId;
20 | this.moduleResolver = moduleResolver;
21 | this.shouldMergeExport = false;
22 | this.hasDynamicImport = false;
23 | }
24 | transformers() {
25 | const swapImport = (context) => (rootNode) => {
26 | const visit = (node) => {
27 | node = ts.visitEachChild(node, visit, context);
28 | if (ts.isImportDeclaration(node)) {
29 | return this.transformImport(node);
30 | }
31 | else if (ts.isExportDeclaration(node)) {
32 | return this.transformExportDeclaration(node);
33 | }
34 | else if (ts.isExportAssignment(node)) {
35 | return this.transformExportAssignment(node);
36 | }
37 | else if (ts.isFunctionDeclaration(node)) {
38 | return this.transformExportFunctionDeclaration(node);
39 | }
40 | else if (ts.isVariableStatement(node)) {
41 | return this.transformExportVariableStatement(node);
42 | }
43 | else if (ts.isClassDeclaration(node)) {
44 | return this.transformExportClassDeclaration(node);
45 | }
46 | else if (ts.isEnumDeclaration(node)) {
47 | return this.transformExportEnumDeclaration(node);
48 | }
49 | else if (ts.isCallExpression(node)) {
50 | return this.transformDynamicImport(node);
51 | }
52 | else if (ts.isMetaProperty(node)) {
53 | return this.transformImportMeta(node);
54 | }
55 | return node;
56 | };
57 | return ts.visitNode(rootNode, visit);
58 | };
59 | return [swapImport];
60 | }
61 | normalizeModuleSpecifier(m) {
62 | return this.moduleResolver(this.moduleId, m);
63 | }
64 | transformImport(node) {
65 | const importDecl = node;
66 | const module = this.normalizeModuleSpecifier(importDecl.moduleSpecifier.text);
67 | const importClause = importDecl.importClause;
68 | if (!importClause) {
69 | // import "aaa"
70 | // -> tsb.import("aaa")
71 | return ts.createCall(createTsbImportAccess(), undefined, [
72 | ts.createStringLiteral(module)
73 | ]);
74 | }
75 | const importName = importClause.name;
76 | const bindings = importClause.namedBindings;
77 | const args = ts.createStringLiteral(module);
78 | const importCall = ts.createCall(createTsbImportAccess(), undefined, [
79 | args
80 | ]);
81 | const ret = [];
82 | if (importName) {
83 | // import a from "aa"
84 | // -> const a = __tsbImport("aa").default
85 | ret.push(ts.createVariableStatement(undefined, [
86 | ts.createVariableDeclaration(importName, undefined, ts.createPropertyAccess(importCall, "default"))
87 | ]));
88 | }
89 | if (bindings) {
90 | if (ts.isNamedImports(bindings)) {
91 | // import { a, b } from "aa"
92 | // -> const {a, b} = tsb.import("typescript");
93 | const elements = bindings.elements.map(v => {
94 | if (v.propertyName) {
95 | return ts.createBindingElement(undefined, v.propertyName, v.name);
96 | }
97 | else {
98 | return ts.createBindingElement(undefined, undefined, v.name);
99 | }
100 | });
101 | ret.push(ts.createVariableStatement(undefined, [
102 | ts.createVariableDeclaration(ts.createObjectBindingPattern(elements), undefined, importCall)
103 | ]));
104 | }
105 | else if (ts.isNamespaceImport(bindings)) {
106 | // import * as ts from "typescript"
107 | // -> const ts = tsb.import("typescript");
108 | ret.push(ts.createVariableStatement(undefined, [
109 | ts.createVariableDeclaration(bindings.name, undefined, importCall)
110 | ]));
111 | }
112 | }
113 | return ret;
114 | }
115 | transformDynamicImport(node) {
116 | if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
117 | this.hasDynamicImport = true;
118 | const [module] = node.arguments;
119 | let moduleSpecifier;
120 | if (ts.isStringLiteral(module)) {
121 | moduleSpecifier = ts.createStringLiteral(this.normalizeModuleSpecifier(module.text));
122 | }
123 | else {
124 | moduleSpecifier = createTsbResolveCall(this.moduleId, module);
125 | }
126 | return ts.createCall(createTsbImportDynamicAccess(), node.typeArguments, [
127 | moduleSpecifier
128 | ]);
129 | }
130 | return node;
131 | }
132 | transformExportDeclaration(node) {
133 | this.shouldMergeExport = true;
134 | const exportClause = node.exportClause;
135 | const module = node.moduleSpecifier;
136 | if (exportClause) {
137 | if (module) {
138 | const keyMap = exportClause.elements.map(v => {
139 | let propertyName = v.name.text;
140 | if (v.propertyName) {
141 | propertyName = v.propertyName.text;
142 | }
143 | return ts.createPropertyAssignment(ts.createStringLiteral(propertyName), ts.createStringLiteral(v.name.text));
144 | });
145 | // export {a, b as B} from "..."
146 | // => __export(require("..."), {"a": "a", "b": "B"})
147 | const text = module.text;
148 | return ts.createCall(ts.createIdentifier("__export"), undefined, [
149 | ts.createCall(createTsbImportAccess(), undefined, [
150 | ts.createStringLiteral(this.normalizeModuleSpecifier(text))
151 | ]),
152 | ts.createObjectLiteral(keyMap)
153 | ]);
154 | }
155 | else {
156 | const assignments = exportClause.elements.map(v => {
157 | let propertyName = v.name.text;
158 | if (v.propertyName) {
159 | propertyName = v.propertyName.text;
160 | return ts.createPropertyAssignment(propertyName, ts.createIdentifier(v.name.text));
161 | }
162 | else {
163 | return ts.createShorthandPropertyAssignment(propertyName);
164 | }
165 | });
166 | // export { a, b as B}
167 | // => __export({a, B: b})
168 | return ts.createCall(ts.createIdentifier("__export"), undefined, [
169 | ts.createObjectLiteral(assignments)
170 | ]);
171 | }
172 | }
173 | else {
174 | const text = module.text;
175 | return ts.createCall(ts.createIdentifier("__export"), undefined, [
176 | ts.createCall(createTsbImportAccess(), undefined, [
177 | ts.createStringLiteral(this.normalizeModuleSpecifier(text))
178 | ])
179 | ]);
180 | }
181 | }
182 | transformExportAssignment(node) {
183 | if (node.isExportEquals) {
184 | // export = {}
185 | // -> tsb.exports = {}
186 | return ts.createAssignment(createTsbExportAccess(), node.expression);
187 | }
188 | else {
189 | // export default {}
190 | // -> tsb.exports.default = {}
191 | const name = node.name ? node.name.text : "default";
192 | return ts.createAssignment(ts.createPropertyAccess(createTsbExportAccess(), name), node.expression);
193 | }
194 | }
195 | transformExportFunctionDeclaration(node) {
196 | if (node.modifiers &&
197 | node.modifiers[0].kind === ts.SyntaxKind.ExportKeyword) {
198 | if (node.modifiers[1] &&
199 | node.modifiers[1].kind === ts.SyntaxKind.DefaultKeyword) {
200 | // export default function a() {}
201 | // -> tsb.exports.default = function a() {}
202 | const [_, __, ...rest] = node.modifiers;
203 | return ts.createAssignment(ts.createPropertyAccess(createTsbExportAccess(), "default"), ts.createFunctionExpression([...rest], node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, node.body));
204 | }
205 | else {
206 | // export function a() {}
207 | // ->
208 | // function a() {}
209 | // tsb.exports.a = a;
210 | const [_, ...rest] = node.modifiers;
211 | return [
212 | ts.createFunctionExpression([...rest], node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, node.body),
213 | ts.createAssignment(ts.createPropertyAccess(createTsbExportAccess(), node.name), node.name)
214 | ];
215 | }
216 | }
217 | return node;
218 | }
219 | transformExportVariableStatement(node) {
220 | if (node.modifiers &&
221 | node.modifiers[0].kind === ts.SyntaxKind.ExportKeyword) {
222 | // export const a = {}
223 | // ->
224 | // const a = {}
225 | // export.a = a;
226 | const [_, ...restModifiers] = node.modifiers;
227 | const declarations = ts.createVariableStatement(restModifiers, node.declarationList);
228 | const exprs = node.declarationList.declarations.map(v => {
229 | return ts.createAssignment(ts.createPropertyAccess(createTsbExportAccess(), v.name.text), v.name);
230 | });
231 | return [declarations, ...exprs];
232 | }
233 | return node;
234 | }
235 | transformExportClassDeclaration(node) {
236 | if (node.modifiers &&
237 | node.modifiers[0].kind === ts.SyntaxKind.ExportKeyword) {
238 | let left;
239 | if (node.modifiers[1] &&
240 | node.modifiers[1].kind === ts.SyntaxKind.DefaultKeyword) {
241 | // export default class Class {}
242 | // -> tsb.exports.default = Class
243 | left = ts.createPropertyAccess(createTsbExportAccess(), "default");
244 | }
245 | else {
246 | // export class Class{}
247 | // -> tsb.exports.Class = Class;
248 | left = ts.createPropertyAccess(createTsbExportAccess(), node.name);
249 | }
250 | return ts.createAssignment(left, ts.createClassExpression(undefined, node.name, node.typeParameters, node.heritageClauses, node.members));
251 | }
252 | return node;
253 | }
254 | transformExportEnumDeclaration(node) {
255 | if (node.modifiers &&
256 | node.modifiers[0].kind === ts.SyntaxKind.ExportKeyword) {
257 | // export enum Enum {}
258 | // -> enum Enum {}, tsb.exports.Enum = Enum
259 | const [_, ...rest] = node.modifiers;
260 | return [
261 | ts.createEnumDeclaration(node.decorators, [...rest], node.name, node.members),
262 | ts.createAssignment(ts.createPropertyAccess(createTsbExportAccess(), node.name), node.name)
263 | ];
264 | }
265 | return [node];
266 | }
267 | transformImportMeta(node) {
268 | return ts.createPropertyAccess(ts.createIdentifier("tsb"), "meta");
269 | }
270 | }
271 | exports.Transformer = Transformer;
272 |
--------------------------------------------------------------------------------
/example/dynamic_import.ts:
--------------------------------------------------------------------------------
1 | async function func() {
2 | import("./other.ts").then(async other => {
3 | console.log(other.callNever());
4 | const color = await import("https://deno.land/std@v0.15.0/colors/mod.ts");
5 | console.log(color);
6 | });
7 | // const a = "./other.ts";
8 | const b = "https://deno.land/std@v0.15.0/colors/mod.ts";
9 | // console.log(await import(a));
10 | console.log(await import(b));
11 | }
12 | func();
13 |
--------------------------------------------------------------------------------
/example/equal.ts:
--------------------------------------------------------------------------------
1 | export = { equal: 1 };
2 |
--------------------------------------------------------------------------------
/example/example.ts:
--------------------------------------------------------------------------------
1 | import { serve } from "https://deno.land/std@v0.15.0/http/server.ts";
2 | import { cyan } from "https://deno.land/std@v0.15.0/colors/mod.ts";
3 | import some from "./some.ts";
4 | import { callOther, callNever as callAnother, callNever } from "./other.ts";
5 | import * as hoge from "./hoge.ts";
6 | import "./dynamic_import.ts";
7 | import other, * as otherAll from "./other.ts";
8 | import other2, { callNever as callNever2 } from "./other.ts";
9 |
10 | export default { a: 1 };
11 | export const f = 2;
12 | export function func() {}
13 | export * from "./some.ts";
14 | export { a, b, c as CC } from "./hoge.ts";
15 | export class SomeClass {}
16 | export enum Fuga {
17 | a = 1
18 | }
19 | enum Hoge {
20 | h = 1,
21 | v = 2
22 | }
23 | export { Hoge };
24 | export let variable, variable2;
25 |
26 | console.log(cyan(callOther() + ":" + callNever()));
27 |
--------------------------------------------------------------------------------
/example/hoge.ts:
--------------------------------------------------------------------------------
1 | export const a = 1;
2 | export const b = 2;
3 | export const c = 3;
4 |
--------------------------------------------------------------------------------
/example/import_meta.ts:
--------------------------------------------------------------------------------
1 | console.log(import.meta.url);
--------------------------------------------------------------------------------
/example/nodelib.ts:
--------------------------------------------------------------------------------
1 | import Debug from "https://dev.jspm.io/debug";
2 | import colors from "https://dev.jspm.io/colors";
3 |
4 | const debug = Debug("tsb");
5 | debug("TypeScript");
6 | console.log(colors.green("green"));
7 |
--------------------------------------------------------------------------------
/example/other.ts:
--------------------------------------------------------------------------------
1 | export function callOther(): string {
2 | return "other";
3 | }
4 |
5 | export function callNever(): string {
6 | return "never";
7 | }
8 |
9 | export default function() {
10 | return "default";
11 | }
12 |
--------------------------------------------------------------------------------
/example/react.tsx:
--------------------------------------------------------------------------------
1 | import React from "https://dev.jspm.io/react"
2 | import ReactDOM from "https://dev.jspm.io/react-dom"
3 |
4 | const View = () => {
5 | const [now, setNow] = React.useState(new Date());
6 | return (
7 |
8 | now: {now.toISOString()}
9 |
10 |
11 | )
12 | };
13 |
14 | window.addEventListener("DOMContentLoaded", () => {
15 | ReactDOM.render(, document.body);
16 | });
17 |
--------------------------------------------------------------------------------
/example/server.ts:
--------------------------------------------------------------------------------
1 | import { listenAndServe } from "https://denopkg.com/keroxp/servest@v0.9.0/server.ts";
2 | listenAndServe(":8899", async req => {
3 | await req.respond({
4 | status: 200,
5 | headers: new Headers({
6 | "Content-Type": "text/plain"
7 | }),
8 | body: new TextEncoder().encode("hello")
9 | });
10 | });
11 | console.log("server is running on :8899...");
12 |
--------------------------------------------------------------------------------
/example/some.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | prop: 1
3 | };
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@keroxp/tsb",
3 | "version": "0.8.1",
4 | "publishConfig": {
5 | "access": "public"
6 | },
7 | "bin": "./dist/main.js",
8 | "description": "TypeScript module bundler for Deno",
9 | "main": "./dist/main.js",
10 | "repository": "https://github.com/keroxp/tsb",
11 | "author": "keroxp ",
12 | "license": "MIT",
13 | "scripts": {
14 | "b": "tsc",
15 | "fmt": "prettier --write '**/*.ts'",
16 | "test": "jest --forceExit"
17 | },
18 | "jest": {
19 | "preset": "ts-jest"
20 | },
21 | "dependencies": {
22 | "cachedir": "^2.2.0",
23 | "caporal": "^1.3.0",
24 | "colors": "^1.3.3",
25 | "node-fetch": "^2.6.0",
26 | "typescript": "^3.5.3",
27 | "fs-extra": "^8.1.0"
28 | },
29 | "devDependencies": {
30 | "@types/node-fetch": "^2.5.0",
31 | "@types/fs-extra": "^8.0.0",
32 | "@types/jest": "^24.0.17",
33 | "@types/node": "^12.7.1",
34 | "jest": "^24.8.0",
35 | "prettier": "^1.18.2",
36 | "ts-jest": "^24.0.2"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/__tests__/bundle.test.ts:
--------------------------------------------------------------------------------
1 | import { joinModuleId } from "../bundle";
2 | import * as ts from "typescript";
3 | describe("bundle", () => {
4 | describe("normalizeModule", () => {
5 | test("any + url", () => {
6 | const res = joinModuleId({
7 | moduleId: "https://deno.land/hoge.ts",
8 | dependency: "https://deno.land/some.ts"
9 | });
10 | expect(res).toBe("https://deno.land/some.ts");
11 | });
12 | test("url + relative", () => {
13 | const res = joinModuleId({
14 | moduleId: "https://deno.land/hoge.ts",
15 | dependency: "./some.ts"
16 | });
17 | expect(res).toBe("https://deno.land/some.ts");
18 | });
19 | test("relative + relative", () => {
20 | const res = joinModuleId({
21 | moduleId: "./example/hoge.ts",
22 | dependency: "./some.ts"
23 | });
24 | expect(res).toBe("./example/some.ts");
25 | });
26 | test("relative + relative (subdir)", () => {
27 | const res = joinModuleId({
28 | moduleId: "./example/subdir/hoge.ts",
29 | dependency: "../otherdir/some.ts"
30 | });
31 | expect(res).toBe("./example/otherdir/some.ts");
32 | });
33 | test("root", () => {
34 | const res = joinModuleId({
35 | moduleId: ".",
36 | dependency: "./example/some.ts"
37 | });
38 | expect(res).toBe("./example/some.ts");
39 | });
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/src/__tests__/fetch.test.ts:
--------------------------------------------------------------------------------
1 | import { urlToCacheFilePath, urlToCacheMetaFilePath } from "../fetch";
2 | import * as cachedir from "cachedir";
3 | import * as path from "path";
4 | describe("fetch", () => {
5 | test("urlToCachePath", () => {
6 | const url = "https://deno.land/sub/dir/script.ts?query=1";
7 | const res = urlToCacheFilePath(url);
8 | const exp = path.resolve(
9 | cachedir("tsb"),
10 | "https",
11 | "deno.land",
12 | // /sub/dir/script.ts?query=1
13 | "118a6a93e2c6de545787b444e91ef3906f20688e4b47a55cb28f14a05e51dcab"
14 | );
15 | expect(res).toBe(exp);
16 | expect(urlToCacheMetaFilePath(url)).toBe(exp + ".meta.json");
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/__tests__/transform.test.ts:
--------------------------------------------------------------------------------
1 | import { Transformer } from "../transform";
2 | import * as ts from "typescript";
3 |
4 | describe("transform", () => {
5 | const printer = ts.createPrinter();
6 | const t = new Transformer("./example.ts", (moduleId, dep) => {
7 | return dep;
8 | });
9 |
10 | function transform(text: string): string {
11 | const src = ts.createSourceFile("example.ts", text, ts.ScriptTarget.ESNext);
12 | const result = ts.transform(src, t.transformers());
13 | return printer.printFile(result.transformed[0] as ts.SourceFile);
14 | }
15 |
16 | describe("import", () => {
17 | test("namedImport(relative)", () => {
18 | const res = transform(`import { a } from "./some.ts";\n`);
19 | expect(res).toBe(`var { a } = tsb.import("./some.ts");\n`);
20 | });
21 | test("namedImport(url)", () => {
22 | const res = transform(
23 | `import { serve } from "https://deno.land/std/http/server.ts";\n`
24 | );
25 | expect(res).toBe(
26 | `var { serve } = tsb.import("https://deno.land/std/http/server.ts");\n`
27 | );
28 | });
29 | test("binding", () => {
30 | const res = transform(
31 | `import { serve as doServe } from "https://deno.land/std/http/server.ts";\n`
32 | );
33 | expect(res).toBe(
34 | `var { serve: doServe } = tsb.import("https://deno.land/std/http/server.ts");\n`
35 | );
36 | });
37 | test("*", () => {
38 | const res = transform(
39 | `import * as http from "https://deno.land/std/http/server.ts";\n`
40 | );
41 | expect(res).toBe(
42 | `var http = tsb.import("https://deno.land/std/http/server.ts");\n`
43 | );
44 | });
45 | test("default", () => {
46 | const res = transform(
47 | `import http from "https://deno.land/std/http/server.ts";\n`
48 | );
49 | expect(res).toBe(
50 | `var http = tsb.import("https://deno.land/std/http/server.ts").default;\n`
51 | );
52 | });
53 | test("default + namedImport", () => {
54 | const res = transform(
55 | `import http, { some, other as doOther } from "https://deno.land/std/http/server.ts";\n`
56 | );
57 | // prettier-ignore
58 | expect(res).toBe(
59 | `var http = tsb.import("https://deno.land/std/http/server.ts").default;
60 | var { some, other: doOther } = tsb.import("https://deno.land/std/http/server.ts");
61 | `
62 | );
63 | });
64 | test("default + namespace", () => {
65 | const res = transform(
66 | `import http, * as all from "https://deno.land/std/http/server.ts";\n`
67 | );
68 | // prettier-ignore
69 | expect(res).toBe(
70 | `var http = tsb.import("https://deno.land/std/http/server.ts").default;
71 | var all = tsb.import("https://deno.land/std/http/server.ts");
72 | `
73 | );
74 | });
75 | test("dynamic", () => {
76 | const res = transform(`import("hoge").then(v => { })`);
77 | expect(res).toBe(`tsb.importDynamic("hoge").then(v => { });\n`);
78 | });
79 | test("unassigned", () => {
80 | const res = transform(`import "aa"`);
81 | expect(res).toBe(`tsb.import("aa")\n`);
82 | });
83 | });
84 | describe("import.meta", () => {
85 | [
86 | ["import.meta.url", "tsb.meta.url;\n"],
87 | ["import.meta.isMain", "tsb.meta.isMain;\n"],
88 | ["import.meta", "tsb.meta;\n"]
89 | ].forEach(([before, after]) => {
90 | expect(transform(before)).toBe(after);
91 | });
92 | });
93 | describe("export", () => {
94 | test("named", () => {
95 | const res = transform(`export { Hoge }`);
96 | expect(res).toBe("__export({ Hoge })\n");
97 | });
98 | test("default", () => {
99 | const res = transform("export default 1");
100 | expect(res).toBe("tsb.exports.default = 1\n");
101 | });
102 | test("default function(anonymous)", () => {
103 | const res = transform("export default function () { }");
104 | expect(res).toBe("tsb.exports.default = function () { }\n");
105 | });
106 | test("default function(named)", () => {
107 | const res = transform("export default function func() { }");
108 | expect(res).toBe("tsb.exports.default = function func() { }\n");
109 | });
110 | test("default class", () => {
111 | const res = transform("export default class Class {};\n");
112 | expect(res).toBe("tsb.exports.default = class Class {\n}\n;\n");
113 | });
114 | test("variable", () => {
115 | const res = transform("export const kon = 1;\n");
116 | expect(res).toBe("const kon = 1;\ntsb.exports.kon = kon\n");
117 | });
118 | test("function", () => {
119 | const res = transform("export function func() {};\n");
120 | expect(res).toBe("function func() { }\ntsb.exports.func = func\n;\n");
121 | });
122 | test("class", () => {
123 | const res = transform("export class Class {};\n");
124 | expect(res).toBe("tsb.exports.Class = class Class {\n}\n;\n");
125 | });
126 | test("enum", () => {
127 | const res = transform("export enum Enum {};\n");
128 | expect(res).toBe("enum Enum {\n}\ntsb.exports.Enum = Enum\n;\n");
129 | });
130 | test("assignment", () => {
131 | const res = transform(`export * from "./other.ts"`);
132 | expect(res).toBe(`__export(tsb.import("./other.ts"))\n`);
133 | });
134 | test("named with module specifier", () => {
135 | const res = transform(`export { Hoge, Fuga as fuga } from "hoge"`);
136 | expect(res).toBe(
137 | `__export(tsb.import("hoge"), { "Hoge": "Hoge", "Fuga": "fuga" })\n`
138 | );
139 | });
140 | test("default with module specifier", () => {
141 | const res = transform(`export { default } from "hoge"`);
142 | expect(res).toBe(
143 | `__export(tsb.import("hoge"), { "default": "default" })\n`
144 | );
145 | });
146 | test("equal", () => {
147 | const res = transform("export = {}");
148 | expect(res).toBe("tsb.exports = {}\n");
149 | });
150 | });
151 |
152 | test("a", () => {
153 | const v = (n:ts.Node) => {
154 | console.log(ts.SyntaxKind[n.kind],n)
155 | n.forEachChild(v)
156 | };
157 | ts.forEachChild(
158 | ts.createSourceFile("", "import.meta", ts.ScriptTarget.ESNext),
159 | v,
160 | )
161 | })
162 | });
163 |
--------------------------------------------------------------------------------
/src/bundle.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Yusuke Sakurai. All rights reserved. MIT license.
2 | import * as path from "path";
3 | import * as url from "url";
4 | import { Transformer } from "./transform";
5 | import * as fs from "fs-extra";
6 | import * as ts from "typescript";
7 | import {
8 | CacheFileMetadata,
9 | fetchModule,
10 | urlToCacheFilePath,
11 | urlToCacheMetaFilePath
12 | } from "./fetch";
13 | import { CliOptions } from "./main";
14 |
15 | export const kUriRegex = /^(https?):\/\/(.+?)$/;
16 | export const kRelativeRegex = /^\.\.?\/.+?\.[jt]sx?$/;
17 |
18 | async function readFileAsync(file: string): Promise {
19 | return String(await fs.readFile(file));
20 | }
21 |
22 | export type SourceFile = {
23 | moduleId: string;
24 | dependency: string;
25 | };
26 |
27 | async function fileExists(p: string): Promise {
28 | return fs.pathExists(p);
29 | }
30 |
31 | async function resolveUri(id: string): Promise {
32 | if (id.match(kUriRegex)) {
33 | return urlToCacheFilePath(id);
34 | } else if (id.match(kRelativeRegex)) {
35 | return path.resolve(id);
36 | } else {
37 | throw new Error("invalid module specifier: " + id);
38 | }
39 | }
40 |
41 | export async function resolveModuleId(
42 | source: SourceFile,
43 | skipFetch = false
44 | ): Promise {
45 | if (source.dependency.match(kUriRegex)) {
46 | // any + url
47 | const cachePath = urlToCacheFilePath(source.dependency);
48 | const cacheMetaPath = urlToCacheMetaFilePath(source.dependency);
49 | if (!(await fileExists(cachePath))) {
50 | if (!(await fileExists(cacheMetaPath))) {
51 | if (!skipFetch) {
52 | await fetchModule(source.dependency);
53 | return resolveModuleId(source, false);
54 | } else {
55 | throw createError(
56 | source,
57 | `
58 | Cache file was not found in: ${cachePath}.
59 | `
60 | );
61 | }
62 | }
63 | const headers = await readFileAsync(cacheMetaPath);
64 | const meta = JSON.parse(headers) as CacheFileMetadata;
65 | if (!meta.redirectTo) {
66 | throw new Error(`meta file for ${source.dependency} may be broken`);
67 | }
68 | return resolveModuleId({
69 | moduleId: ".",
70 | dependency: meta.redirectTo
71 | });
72 | } else {
73 | return source.dependency;
74 | }
75 | } else if (source.moduleId.match(kUriRegex)) {
76 | // url + relative
77 | return resolveModuleId({
78 | moduleId: ".",
79 | dependency: url.resolve(source.moduleId, source.dependency)
80 | });
81 | } else {
82 | // relative + relative
83 | return joinModuleId(source);
84 | }
85 | }
86 |
87 | async function traverseDependencyTree(
88 | sourceFile: SourceFile,
89 | dependencyTree: Map,
90 | redirectionMap: Map,
91 | opts: CliOptions
92 | ): Promise {
93 | const dependencies: string[] = [];
94 | let id: string;
95 | id = await resolveModuleId(sourceFile, opts.skipFetch);
96 | redirectionMap.set(joinModuleId(sourceFile), id);
97 | if (dependencyTree.has(id)) {
98 | return;
99 | }
100 | dependencyTree.set(id, sourceFile);
101 |
102 | const visit = (node: ts.Node) => {
103 | if (ts.isImportDeclaration(node)) {
104 | const dependency = (node.moduleSpecifier as ts.StringLiteral).text;
105 | dependencies.push(dependency);
106 | } else if (
107 | ts.isCallExpression(node) &&
108 | node.expression.kind === ts.SyntaxKind.ImportKeyword
109 | ) {
110 | // import("aa").then(v => {})
111 | const [module] = node.arguments;
112 | if (ts.isStringLiteral(module)) {
113 | dependencies.push(module.text);
114 | }
115 | } else if (ts.isExportDeclaration(node)) {
116 | const exportClause = node.exportClause;
117 | const module = node.moduleSpecifier;
118 | if (exportClause) {
119 | if (module) {
120 | // export {a,b} form "bb"
121 | dependencies.push((module as ts.StringLiteral).text);
122 | } else {
123 | // export {a,b
124 | }
125 | } else {
126 | dependencies.push((module as ts.StringLiteral).text);
127 | }
128 | }
129 | ts.forEachChild(node, visit);
130 | };
131 |
132 | const resolvedPath = await resolveUri(id);
133 | const text = await readFileAsync(resolvedPath);
134 | const src = ts.createSourceFile(resolvedPath, text, ts.ScriptTarget.ESNext);
135 | ts.forEachChild(src, visit);
136 | for (const dependency of dependencies) {
137 | await traverseDependencyTree(
138 | { dependency: dependency, moduleId: id },
139 | dependencyTree,
140 | redirectionMap,
141 | opts
142 | );
143 | }
144 | }
145 |
146 | export function joinModuleId(source: SourceFile): string {
147 | if (source.dependency.match(kUriRegex)) {
148 | // url
149 | return source.dependency;
150 | } else if (source.moduleId.match(kUriRegex)) {
151 | // url + relative
152 | return url.resolve(source.moduleId, source.dependency);
153 | } else if (source.dependency.match(kRelativeRegex)) {
154 | // relative + relative
155 | const cwd = process.cwd();
156 | const dir = path.dirname(source.moduleId);
157 | return "./" + path.relative(cwd, path.join(dir, source.dependency));
158 | } else {
159 | throw createError(source, `dependency must be URL or start with ./ or ../`);
160 | }
161 | }
162 |
163 | function createError(source: SourceFile, message: string): Error {
164 | return new Error(
165 | `moduleId: "${source.moduleId}", dependency: "${source.dependency}": ${message}`
166 | );
167 | }
168 |
169 | export async function bundle(entry: string, opts: CliOptions) {
170 | const tree = new Map();
171 | const redirectionMap = new Map();
172 | let canonicalName: string;
173 | if (entry.match(kUriRegex)) {
174 | canonicalName = entry;
175 | } else {
176 | canonicalName = "./" + path.relative(process.cwd(), entry);
177 | }
178 | await traverseDependencyTree(
179 | {
180 | dependency: canonicalName,
181 | moduleId: "."
182 | },
183 | tree,
184 | redirectionMap,
185 | opts
186 | );
187 | const printer = ts.createPrinter();
188 | let template = await readFileAsync(
189 | path.resolve(__dirname, "../template/template.ts")
190 | );
191 | const resolveModule = (moduleId: string, dep: string): string => {
192 | const redirection = redirectionMap.get(moduleId);
193 | if (!redirection) {
194 | throw new Error(`${moduleId} not found in redirection map`);
195 | }
196 | if (dep.match(kUriRegex)) {
197 | const ret = redirectionMap.get(dep);
198 | if (!ret) {
199 | throw new Error(`${dep} not found in redirection map`);
200 | }
201 | return ret;
202 | } else {
203 | return joinModuleId({
204 | moduleId: moduleId,
205 | dependency: dep
206 | });
207 | }
208 | };
209 | const modules: string[] = [];
210 | for (const [moduleId] of tree.entries()) {
211 | const transformer = new Transformer(moduleId, resolveModule);
212 | let text = await readFileAsync(await resolveUri(moduleId));
213 | if (text.startsWith("#!")) {
214 | // disable shell
215 | text = "//" + text;
216 | }
217 | const src = ts.createSourceFile(moduleId, text, ts.ScriptTarget.ESNext);
218 | const result = ts.transform(src, transformer.transformers());
219 | const transformed = printer.printFile(result
220 | .transformed[0] as ts.SourceFile);
221 | const opts: ts.CompilerOptions = {
222 | target: ts.ScriptTarget.ESNext
223 | };
224 | if (moduleId.endsWith(".tsx") || moduleId.endsWith(".jsx")) {
225 | opts.jsx = ts.JsxEmit.React;
226 | }
227 | let body = ts.transpile(transformed, opts);
228 | if (transformer.shouldMergeExport) {
229 | body = `
230 | function __export(m,k) {
231 | if (k) {
232 | for (const p in k) if (!tsb.exports.hasOwnProperty(k[p])) tsb.exports[k[p]] = m[p];
233 | } else {
234 | for (const p in m) if (!tsb.exports.hasOwnProperty(p)) tsb.exports[p] = m[p];
235 | }
236 | }
237 | ${body}
238 | `;
239 | }
240 | modules.push(`"${moduleId}": function (tsb) { ${body} } `);
241 | }
242 | const body = modules.join(",");
243 | const entryId = await resolveModuleId({
244 | dependency: canonicalName,
245 | moduleId: "."
246 | });
247 | template = `(${template}).call(this, {${body}}, "${entryId}")`;
248 | const output = ts.transpile(template, {
249 | target: ts.ScriptTarget.ESNext
250 | });
251 | console.log(output);
252 | }
253 |
--------------------------------------------------------------------------------
/src/fetch.ts:
--------------------------------------------------------------------------------
1 | import { default as fetch } from "node-fetch";
2 | import { URL } from "url";
3 | import * as cachdir from "cachedir";
4 | import * as path from "path";
5 | import * as fs from "fs-extra";
6 | import { green } from "colors";
7 | import * as crypto from "crypto";
8 |
9 | const cacheDirectory = cachdir("tsb");
10 |
11 | export function urlToCacheFilePath(url: string): string {
12 | const u = new URL(url);
13 | if (!u.protocol.match(/^https?/)) {
14 | throw new Error("url must start with https?:" + url);
15 | }
16 | const fullPath = u.pathname + u.search;
17 | const scheme = u.protocol.startsWith("https") ? "https" : "http";
18 | const sha256 = crypto.createHash("sha256");
19 | sha256.update(fullPath);
20 | const fullPathHash = sha256.digest("hex");
21 | // ~/Library/Caches/tsb/https/deno.land/{sha256hashOfUrl}
22 | return path.join(cacheDirectory, scheme, u.host, fullPathHash);
23 | }
24 |
25 | export function urlToCacheMetaFilePath(url: string): string {
26 | // ~/Library/Caches/tsb/https/deno.land/{sha256hashOfUrl}.meta.json
27 | return urlToCacheFilePath(url) + ".meta.json";
28 | }
29 |
30 | export type CacheFileMetadata = {
31 | redirectTo?: string;
32 | mimeType?: string;
33 | originalPath: string;
34 | };
35 |
36 | async function saveMetaFile(
37 | url: string,
38 | meta: CacheFileMetadata
39 | ): Promise {
40 | const dest = urlToCacheMetaFilePath(url);
41 | await fs.writeFile(dest, JSON.stringify(meta));
42 | }
43 |
44 | const kAcceptableMimeTypes = [
45 | "text/plain",
46 | "application/javascript",
47 | "text/javascript",
48 | "application/typescript",
49 | "text/typescript"
50 | ];
51 |
52 | export async function fetchModule(url: string): Promise {
53 | const u = new URL(url);
54 | const originalPath = u.pathname + u.search;
55 | const dest = urlToCacheFilePath(url);
56 | console.error(`${green("Download")} ${url}`);
57 | const resp = await fetch(url, {
58 | method: "GET",
59 | redirect: "manual"
60 | });
61 | const dir = path.dirname(dest);
62 | if (!(await fs.pathExists(dir))) {
63 | await fs.ensureDir(dir);
64 | }
65 | if (400 <= resp.status) {
66 | throw new Error(`fetch failed with status code ${resp.status}`);
67 | }
68 | if (200 <= resp.status && resp.status < 300) {
69 | const contentType = resp.headers.get("content-type") || "";
70 | if (!kAcceptableMimeTypes.some(v => contentType.startsWith(v))) {
71 | throw new Error(`unacceptable content-type for ${url}: ${contentType} `);
72 | }
73 | await Promise.all([
74 | // TODO: pipe body stream
75 | fs.writeFile(dest, await resp.text()),
76 | saveMetaFile(url, { mimeType: contentType, originalPath })
77 | ]);
78 | } else if (300 <= resp.status) {
79 | const redirectTo = resp.headers.get("location");
80 | if (!redirectTo) {
81 | throw new Error("redirected response didn't has Location headers!");
82 | }
83 | await saveMetaFile(url, {
84 | redirectTo,
85 | originalPath
86 | });
87 | return fetchModule(redirectTo);
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Copyright 2019 Yusuke Sakurai. All rights reserved. MIT license.
3 | import * as caporal from "caporal";
4 | import { bundle } from "./bundle";
5 |
6 | caporal
7 | .name("tsb")
8 | .version("0.8.1")
9 | .argument("file", "entry file path for bundle")
10 | .option("--skipFetch", "skip fetching remote module recursively")
11 | .action(action);
12 |
13 | export type CliOptions = {
14 | skipFetch: boolean;
15 | };
16 |
17 | async function action(args: { file: string }, opts: CliOptions) {
18 | try {
19 | await bundle(args.file, opts);
20 | } catch (e) {
21 | if (e instanceof Error) {
22 | console.error(e.stack);
23 | }
24 | }
25 | }
26 |
27 | if (require.main) {
28 | caporal.parse(process.argv);
29 | }
30 |
--------------------------------------------------------------------------------
/src/transform.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Yusuke Sakurai. All rights reserved. MIT license.
2 | import * as ts from "typescript";
3 |
4 | function createTsbImportAccess(): ts.Expression {
5 | return ts.createPropertyAccess(ts.createIdentifier("tsb"), "import");
6 | }
7 | function createTsbImportDynamicAccess(): ts.Expression {
8 | return ts.createPropertyAccess(ts.createIdentifier("tsb"), "importDynamic");
9 | }
10 | function createTsbExportAccess(): ts.Expression {
11 | return ts.createPropertyAccess(ts.createIdentifier("tsb"), "exports");
12 | }
13 | function createTsbResolveCall(
14 | module: string,
15 | dep: ts.Expression
16 | ): ts.Expression {
17 | return ts.createCall(
18 | ts.createPropertyAccess(ts.createIdentifier("tsb"), "resolveModule"),
19 | undefined,
20 | [ts.createStringLiteral(module), dep]
21 | );
22 | }
23 | export class Transformer {
24 | shouldMergeExport: boolean = false;
25 | hasDynamicImport = false;
26 |
27 | constructor(
28 | readonly moduleId: string,
29 | readonly moduleResolver: (moduleId: string, dep: string) => string
30 | ) {}
31 |
32 | transformers() {
33 | const swapImport = (
34 | context: ts.TransformationContext
35 | ) => (rootNode: T) => {
36 | const visit = (node: ts.Node): ts.VisitResult => {
37 | node = ts.visitEachChild(node, visit, context);
38 | if (ts.isImportDeclaration(node)) {
39 | return this.transformImport(node);
40 | } else if (ts.isExportDeclaration(node)) {
41 | return this.transformExportDeclaration(node);
42 | } else if (ts.isExportAssignment(node)) {
43 | return this.transformExportAssignment(node);
44 | } else if (ts.isFunctionDeclaration(node)) {
45 | return this.transformExportFunctionDeclaration(node);
46 | } else if (ts.isVariableStatement(node)) {
47 | return this.transformExportVariableStatement(node);
48 | } else if (ts.isClassDeclaration(node)) {
49 | return this.transformExportClassDeclaration(node);
50 | } else if (ts.isEnumDeclaration(node)) {
51 | return this.transformExportEnumDeclaration(node);
52 | } else if (ts.isCallExpression(node)) {
53 | return this.transformDynamicImport(node);
54 | } else if (ts.isMetaProperty(node)) {
55 | return this.transformImportMeta(node);
56 | }
57 | return node;
58 | };
59 | return ts.visitNode(rootNode, visit);
60 | };
61 | return [swapImport];
62 | }
63 |
64 | normalizeModuleSpecifier(m: string): string {
65 | return this.moduleResolver(this.moduleId, m);
66 | }
67 |
68 | transformImport(node: ts.ImportDeclaration): ts.VisitResult {
69 | const importDecl: ts.ImportDeclaration = node;
70 | const module = this.normalizeModuleSpecifier(
71 | (importDecl.moduleSpecifier as ts.StringLiteral).text
72 | );
73 | const importClause = importDecl.importClause;
74 | if (!importClause) {
75 | // import "aaa"
76 | // -> tsb.import("aaa")
77 | return ts.createCall(createTsbImportAccess(), undefined, [
78 | ts.createStringLiteral(module)
79 | ]);
80 | }
81 | const importName = importClause.name;
82 | const bindings = importClause!.namedBindings;
83 | const args = ts.createStringLiteral(module);
84 | const importCall = ts.createCall(createTsbImportAccess(), undefined, [
85 | args
86 | ]);
87 | const ret: ts.Node[] = [];
88 | if (importName) {
89 | // import a from "aa"
90 | // -> const a = __tsbImport("aa").default
91 | ret.push(
92 | ts.createVariableStatement(undefined, [
93 | ts.createVariableDeclaration(
94 | importName,
95 | undefined,
96 | ts.createPropertyAccess(importCall, "default")
97 | )
98 | ])
99 | );
100 | }
101 | if (bindings) {
102 | if (ts.isNamedImports(bindings)) {
103 | // import { a, b } from "aa"
104 | // -> const {a, b} = tsb.import("typescript");
105 | const elements = bindings.elements.map(v => {
106 | if (v.propertyName) {
107 | return ts.createBindingElement(undefined, v.propertyName, v.name);
108 | } else {
109 | return ts.createBindingElement(undefined, undefined, v.name);
110 | }
111 | });
112 | ret.push(
113 | ts.createVariableStatement(undefined, [
114 | ts.createVariableDeclaration(
115 | ts.createObjectBindingPattern(elements),
116 | undefined,
117 | importCall
118 | )
119 | ])
120 | );
121 | } else if (ts.isNamespaceImport(bindings)) {
122 | // import * as ts from "typescript"
123 | // -> const ts = tsb.import("typescript");
124 | ret.push(
125 | ts.createVariableStatement(undefined, [
126 | ts.createVariableDeclaration(bindings.name, undefined, importCall)
127 | ])
128 | );
129 | }
130 | }
131 | return ret;
132 | }
133 |
134 | transformDynamicImport(node: ts.CallExpression): ts.Node {
135 | if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
136 | this.hasDynamicImport = true;
137 | const [module] = node.arguments;
138 | let moduleSpecifier: ts.Expression;
139 | if (ts.isStringLiteral(module)) {
140 | moduleSpecifier = ts.createStringLiteral(
141 | this.normalizeModuleSpecifier(module.text)
142 | );
143 | } else {
144 | moduleSpecifier = createTsbResolveCall(this.moduleId, module);
145 | }
146 | return ts.createCall(createTsbImportDynamicAccess(), node.typeArguments, [
147 | moduleSpecifier
148 | ]);
149 | }
150 | return node;
151 | }
152 |
153 | transformExportDeclaration(
154 | node: ts.ExportDeclaration
155 | ): ts.VisitResult {
156 | this.shouldMergeExport = true;
157 | const exportClause = node.exportClause;
158 | const module = node.moduleSpecifier;
159 | if (exportClause) {
160 | if (module) {
161 | const keyMap = exportClause.elements.map(v => {
162 | let propertyName: string = v.name.text;
163 | if (v.propertyName) {
164 | propertyName = v.propertyName.text;
165 | }
166 | return ts.createPropertyAssignment(
167 | ts.createStringLiteral(propertyName),
168 | ts.createStringLiteral(v.name.text)
169 | );
170 | });
171 | // export {a, b as B} from "..."
172 | // => __export(require("..."), {"a": "a", "b": "B"})
173 | const text = (module as ts.StringLiteral).text;
174 | return ts.createCall(ts.createIdentifier("__export"), undefined, [
175 | ts.createCall(createTsbImportAccess(), undefined, [
176 | ts.createStringLiteral(this.normalizeModuleSpecifier(text))
177 | ]),
178 | ts.createObjectLiteral(keyMap)
179 | ]);
180 | } else {
181 | const assignments = exportClause.elements.map(v => {
182 | let propertyName: string = v.name.text;
183 | if (v.propertyName) {
184 | propertyName = v.propertyName.text;
185 | return ts.createPropertyAssignment(
186 | propertyName,
187 | ts.createIdentifier(v.name.text)
188 | );
189 | } else {
190 | return ts.createShorthandPropertyAssignment(propertyName);
191 | }
192 | });
193 | // export { a, b as B}
194 | // => __export({a, B: b})
195 | return ts.createCall(ts.createIdentifier("__export"), undefined, [
196 | ts.createObjectLiteral(assignments)
197 | ]);
198 | }
199 | } else {
200 | const text = (module as ts.StringLiteral).text;
201 | return ts.createCall(ts.createIdentifier("__export"), undefined, [
202 | ts.createCall(createTsbImportAccess(), undefined, [
203 | ts.createStringLiteral(this.normalizeModuleSpecifier(text))
204 | ])
205 | ]);
206 | }
207 | }
208 |
209 | transformExportAssignment(node: ts.ExportAssignment): ts.Node {
210 | if (node.isExportEquals) {
211 | // export = {}
212 | // -> tsb.exports = {}
213 | return ts.createAssignment(createTsbExportAccess(), node.expression);
214 | } else {
215 | // export default {}
216 | // -> tsb.exports.default = {}
217 | const name = node.name ? node.name.text : "default";
218 | return ts.createAssignment(
219 | ts.createPropertyAccess(createTsbExportAccess(), name),
220 | node.expression
221 | );
222 | }
223 | }
224 |
225 | transformExportFunctionDeclaration(
226 | node: ts.FunctionDeclaration
227 | ): ts.VisitResult {
228 | if (
229 | node.modifiers &&
230 | node.modifiers[0].kind === ts.SyntaxKind.ExportKeyword
231 | ) {
232 | if (
233 | node.modifiers[1] &&
234 | node.modifiers[1].kind === ts.SyntaxKind.DefaultKeyword
235 | ) {
236 | // export default function a() {}
237 | // -> tsb.exports.default = function a() {}
238 | const [_, __, ...rest] = node.modifiers;
239 | return ts.createAssignment(
240 | ts.createPropertyAccess(createTsbExportAccess(), "default"),
241 | ts.createFunctionExpression(
242 | [...rest],
243 | node.asteriskToken,
244 | node.name,
245 | node.typeParameters,
246 | node.parameters,
247 | node.type,
248 | node.body!
249 | )
250 | );
251 | } else {
252 | // export function a() {}
253 | // ->
254 | // function a() {}
255 | // tsb.exports.a = a;
256 | const [_, ...rest] = node.modifiers;
257 | return [
258 | ts.createFunctionExpression(
259 | [...rest],
260 | node.asteriskToken,
261 | node.name,
262 | node.typeParameters,
263 | node.parameters,
264 | node.type,
265 | node.body!
266 | ),
267 | ts.createAssignment(
268 | ts.createPropertyAccess(createTsbExportAccess(), node.name!),
269 | node.name!
270 | )
271 | ];
272 | }
273 | }
274 | return node;
275 | }
276 |
277 | transformExportVariableStatement(node: ts.VariableStatement) {
278 | if (
279 | node.modifiers &&
280 | node.modifiers[0].kind === ts.SyntaxKind.ExportKeyword
281 | ) {
282 | // export const a = {}
283 | // ->
284 | // const a = {}
285 | // export.a = a;
286 | const [_, ...restModifiers] = node.modifiers;
287 | const declarations = ts.createVariableStatement(
288 | restModifiers,
289 | node.declarationList
290 | );
291 | const exprs = node.declarationList.declarations.map(v => {
292 | return ts.createAssignment(
293 | ts.createPropertyAccess(
294 | createTsbExportAccess(),
295 | (v.name as ts.Identifier).text
296 | ),
297 | v.name as ts.Identifier
298 | );
299 | });
300 | return [declarations, ...exprs];
301 | }
302 | return node;
303 | }
304 |
305 | transformExportClassDeclaration(node: ts.ClassDeclaration) {
306 | if (
307 | node.modifiers &&
308 | node.modifiers[0].kind === ts.SyntaxKind.ExportKeyword
309 | ) {
310 | let left: ts.Expression;
311 | if (
312 | node.modifiers[1] &&
313 | node.modifiers[1].kind === ts.SyntaxKind.DefaultKeyword
314 | ) {
315 | // export default class Class {}
316 | // -> tsb.exports.default = Class
317 | left = ts.createPropertyAccess(createTsbExportAccess(), "default");
318 | } else {
319 | // export class Class{}
320 | // -> tsb.exports.Class = Class;
321 | left = ts.createPropertyAccess(createTsbExportAccess(), node.name!);
322 | }
323 | return ts.createAssignment(
324 | left,
325 | ts.createClassExpression(
326 | undefined,
327 | node.name,
328 | node.typeParameters,
329 | node.heritageClauses,
330 | node.members
331 | )
332 | );
333 | }
334 | return node;
335 | }
336 |
337 | transformExportEnumDeclaration(node: ts.EnumDeclaration): ts.Node[] {
338 | if (
339 | node.modifiers &&
340 | node.modifiers[0].kind === ts.SyntaxKind.ExportKeyword
341 | ) {
342 | // export enum Enum {}
343 | // -> enum Enum {}, tsb.exports.Enum = Enum
344 | const [_, ...rest] = node.modifiers;
345 | return [
346 | ts.createEnumDeclaration(
347 | node.decorators,
348 | [...rest],
349 | node.name,
350 | node.members
351 | ),
352 | ts.createAssignment(
353 | ts.createPropertyAccess(createTsbExportAccess(), node.name),
354 | node.name
355 | )
356 | ];
357 | }
358 | return [node];
359 | }
360 |
361 | transformImportMeta(node: ts.MetaProperty): ts.Node {
362 | return ts.createPropertyAccess(
363 | ts.createIdentifier("tsb"),
364 | "meta"
365 | );
366 | }
367 |
368 | }
369 |
--------------------------------------------------------------------------------
/template/template.ts:
--------------------------------------------------------------------------------
1 | function __tsbEntry(modules: { [id: string]: (tsb) => void }, entryId: string) {
2 | interface Tsb {
3 | import(module: string): any;
4 | importDynamic(module: string): Promise;
5 | resolveModule(moduleId: string, dep: string): string;
6 | meta: {
7 | url: string,
8 | isMain: boolean
9 | }
10 | exports: any;
11 | loaded: boolean;
12 | }
13 | const installedModules: Map = new Map();
14 | const uriRegex = /^http?s:\/\//;
15 | const relativeRegex = /^\.\.?\/.+?\.[tj]s$/;
16 | function resolveModule(moduleId: string, dep: string): string {
17 | if (dep.match(uriRegex)) {
18 | // any + url
19 | return dep;
20 | } else if (moduleId.match(uriRegex)) {
21 | // url + regex
22 | return new URL(dep, moduleId).href;
23 | } else if (moduleId.match(relativeRegex)) {
24 | // relative + relative
25 | const stack = moduleId.split("/");
26 | const parts = dep.split("/");
27 | stack.pop();
28 | for (const part of parts) {
29 | if (part === "..") {
30 | stack.pop();
31 | } else if (part !== ".") {
32 | stack.push(part);
33 | }
34 | }
35 | return "./" + stack.join("/");
36 | } else {
37 | throw new Error(`invalid dependency: ${moduleId}, ${dep}`);
38 | }
39 | }
40 | function importInternal(moduleId: string): any {
41 | if (installedModules.has(moduleId)) {
42 | return installedModules.get(moduleId)!.exports;
43 | }
44 | const module: Tsb = {
45 | import: tsbImport,
46 | importDynamic: tsbImportDynamic,
47 | resolveModule,
48 | meta: {
49 | url: new URL(moduleId, "file://").href,
50 | isMain: false
51 | },
52 | loaded: false,
53 | exports: {}
54 | };
55 | installedModules.set(moduleId, module);
56 | // execute module in global context
57 | modules[moduleId].call(this, module);
58 | module.loaded = true;
59 | return module.exports;
60 | }
61 | async function tsbImportDynamic(moduleId): Promise {
62 | if (moduleId[moduleId]) {
63 | return importInternal(moduleId);
64 | } else {
65 | // fallback to dynamic import
66 | return import(moduleId);
67 | }
68 | }
69 | function tsbImport(moduleId): any {
70 | return importInternal(moduleId);
71 | }
72 | return tsbImport(entryId);
73 | }
74 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2018",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "outDir": "dist",
7 | "strictNullChecks": true
8 | },
9 | "include": ["src/**/*.ts"],
10 | "exclude": ["src/**/*.test.ts"]
11 | }
12 |
--------------------------------------------------------------------------------