├── .gitignore
├── .npmignore
├── LICENSE.md
├── README.md
├── karma.conf.js
├── node-libs
└── package.json
├── node-support
├── globals.js
├── package.json
└── stub.js
├── package.json
├── sample-server
└── express.ts
├── src
├── api.ts
├── client
│ ├── loader-bootstrap.ts
│ ├── locator.ts
│ └── system-hooks.ts
├── defaults.ts
├── dir-structure.ts
├── index.ts
├── logger.ts
├── node-support.ts
├── project-mapper.ts
├── types.ts
└── url-resolver.ts
├── test-kit
├── karma-server.ts
├── port.ts
├── project-driver.ts
└── test-server.ts
├── test
├── e2e
│ ├── client-script.ts
│ ├── e2e.spec.ts
│ └── project-fixtures.ts
├── integration
│ ├── api.spec.ts
│ ├── normalize.spec.ts
│ └── remap.spec.ts
└── unit
│ ├── locator.spec.ts
│ ├── project-mapper.spec.ts
│ └── url-resolver.spec.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules/
3 | node-libs/node_modules
4 | dist/
5 | typings/
6 | .vscode/
7 | npm-debug.log
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | dist/test
2 | dist/test-kit
3 | src/
4 | test/
5 | test-kit/
6 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # LICENSE
2 | ## BSD License for Bundless
3 | Copyright (c) 2016, Wix.com Ltd. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
5 | - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
6 | - Neither the name of Wix.com Ltd. nor the names of its contributors may be used to endorse or
7 | promote products derived from this software without specific prior written permission.
8 |
9 |
10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
11 |
12 | # PATENT LICENSE
13 | ## Wix.com Ltd. - Grant of Patent License
14 | "Software" means the Bundless software distributed by Wix.com Ltd. (“Wix”).
15 | A "Necessary Claim" is a claim of a patent licensable by Wix that is necessarily infringed by the Software standing alone.
16 | A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, or contributory infringement or inducement to infringe any patent, including across-claim or counterclaim.
17 | Subject to the terms and conditions of this License and the BSD-style license that can be found in the LICENSE file in the root of the source tree, Wix hereby grants to you a perpetual, worldwide, non- exclusive, no-charge, royalty-free, irrevocable (except as stated below) license under any Necessary Claims, to make, have made, use, offer to sell, sell, import, and otherwise transfer the Software. If you (or any of your subsidiaries, corporate affiliates or agents) institute (either directly or indirectly), or gain a direct financial interest in, a Patent Assertion against
18 | - Wix or any of its subsidiaries or corporate affiliates,
19 | - any person or entity, to the extent such Patent Assertion arises, in whole or in part, from any software, technology, product or service of Wix or any of its subsidiaries or corporate affiliates, or
20 | - any person or entity relating to the Software,
21 |
22 | then the license granted herein shall automatically terminate without any notice, as of the date upon which such Patent Assertion is initiated by you (or any of your subsidiaries, corporate affiliates or agents), or you (or any of your subsidiaries, corporate affiliates or agents) took a financial interest in such Patent Assertion, as the case may be.
23 | Notwithstanding the foregoing, if Wix or any of its subsidiaries or corporate affiliates files a lawsuit alleging patent infringement against you in the first instance, and you file a patent infringement counterclaim unrelated to the Software, in same lawsuit against that claimant, the license granted hereunder will not terminate merely due to such counterclaim.
24 | Notwithstanding anything herein to the contrary, no license is granted under Wix’s rights in any patent claims that are infringed by
25 | - modifications to the Software made by you or any third party or
26 | - the Software in combination with any software or other technology not provided by Wix.
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bundless
2 |
3 | Experimental bundle-free JavaScript dependency loader.
4 |
5 | Bundless is an experimental dependency loader inspired by JSPM, browserify
6 | and webpack, while trying to solve some of their inherent problems. Its
7 | goal is to deliver all JavaScript dependencies to the client without
8 | creating aggregate files ("bundles"), while sticking to npm as the
9 | package manager and to its project structure.
10 |
11 | ## Installation
12 |
13 | `npm install bundless --save`
14 |
15 | ## Usage
16 |
17 | At its core, bundless generates a script and set of hooks which make your
18 | local project accessible to the SystemJS loader without any additional
19 | configuration.
20 |
21 | For an easy start, use the sample ExpressJS router included in the project:
22 |
23 | ```javascript
24 | const bundless = require('bundless/sample-server');
25 | const express = require('express');
26 | const path = require('path');
27 |
28 | const app = express();
29 | const topology = {};
30 | app.use(bundless.express(topology));
31 | app.get('/', (req, res) => res.sendFile(path.resolve(process.cwd(), 'index.html')));
32 |
33 | app.listen(8080, function (err) {
34 | err ? console.error(err) : console.log(`Listening at ${this.address().address}:${this.address().port}`);
35 | });
36 | ```
37 |
38 | Your `/index.html` file should then contain:
39 |
40 | ```html
41 |
42 |
43 |
44 |
48 |
49 | ```
50 |
51 |
52 | (Note that you must have SystemJS installed.)
53 |
54 | Your entry point, in this example, should be then `src/main.js`.
55 |
56 | You can modify your application structure by setting properties of the
57 | `topology` variable:
58 |
59 | ```javascript
60 |
61 | const topology = {
62 | rootDir: process.cwd(),
63 | srcDir: 'src', // Your local .js files, relative to rootDir
64 | srcMount: '/modules', // URL prefix of local files
65 | libMount: '/lib', // URL prefix of libraries (npm dependencies)
66 | nodeMount: '/$node', // Internal URL prefix of Node.js libraries
67 | };
68 | ```
69 |
70 | For more details, check the /sample-server/express.ts file.
71 |
72 | Note, that bundless should work with any static web server, provided
73 | it has been configured according to the topology.
74 |
75 | ## How it works
76 |
77 | Bundless is a set of hooks which lets the browser to resolve and load
78 | dependencies (via SystemJS) in almost exactly the same way as NodeJS does,
79 | with some neat tricks inspired mostly by browserify.
80 |
81 | This, of course, means that for larger projects, we're going to load quite
82 | a bunch of files. This, of course, raises some performance issues, comparing
83 | to "bundled" solutions. So far, we see HTTP/2 serving as a solution,
84 | while researching other possibilities.
85 |
86 | ## Contributing
87 |
88 | Bundless is currently in a wild, alpha, development-and-research stage.
89 | We'll be happy for any comments, opinions, insights, thoughts, pull-requests,
90 | suggestions and bits of wisdom from the community.
91 |
92 | 1. Fork it!
93 | 2. Create your feature branch: `git checkout -b my-new-feature`
94 | 3. Commit your changes: `git commit -am 'Add some feature'`
95 | 4. Push to the branch: `git push origin my-new-feature`
96 | 5. Submit a pull request
97 |
98 | ## License
99 |
100 | See LICENSE.md
101 |
102 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Wed Aug 26 2015 10:52:39 GMT+0300 (IDT)
3 |
4 | var portRangeStart = process.env['CORE3_PORT_RANGE_START'];
5 | var karmaPort = portRangeStart ? parseInt(portRangeStart) + 3: 9876;
6 |
7 | module.exports = function(config) {
8 | config.set({
9 |
10 | // base path that will be used to resolve all patterns (eg. files, exclude)
11 | basePath: '',
12 |
13 |
14 | // frameworks to use
15 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
16 | frameworks: [],
17 |
18 |
19 | // list of files / patterns to load in the browser
20 | files: [
21 | 'node_modules/systemjs/dist/system.js',
22 | 'dist/test/e2e/client-script.js'
23 | ],
24 |
25 |
26 | // list of files to exclude
27 | exclude: [
28 | ],
29 |
30 |
31 | // preprocess matching files before serving them to the browser
32 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
33 | preprocessors: {
34 | },
35 |
36 |
37 | // test results reporter to use
38 | // possible values: 'dots', 'progress'
39 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
40 | reporters: ['env'],
41 |
42 |
43 | // web server port
44 | port: karmaPort,
45 |
46 |
47 | // enable / disable colors in the output (reporters and logs)
48 | colors: true,
49 |
50 |
51 | // level of logging
52 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
53 | logLevel: config.LOG_INFO,
54 |
55 |
56 | // enable / disable watching file and executing tests whenever any file changes
57 | autoWatch: true,
58 |
59 |
60 | // start these browsers
61 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
62 | browsers: ['chrome_without_security'],
63 |
64 | customLaunchers: {
65 | chrome_without_security: {
66 | base: 'Chrome',
67 | flags: ['--disable-web-security', '--ignore-certificate-errors']
68 | }
69 | },
70 |
71 | browserNoActivityTimeout: 1000000,
72 |
73 | // Continuous Integration mode
74 | // if true, Karma captures browsers, runs the tests and exits
75 | singleRun: false
76 | })
77 | }
78 |
--------------------------------------------------------------------------------
/node-libs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-libs-subproject",
3 | "dependencies": {
4 | "node-libs-browser": "1.0.0",
5 | "node-support": "../node-support"
6 | }
7 | }
--------------------------------------------------------------------------------
/node-support/globals.js:
--------------------------------------------------------------------------------
1 | window['Buffer'] = window['Buffer'] || require('buffer').Buffer;
2 | window['process'] = window['process'] || require('process');
3 | process.version = '0.0.0';
4 | process.cwd = function () { return ''; };
5 |
--------------------------------------------------------------------------------
/node-support/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-support",
3 | "version": "0.0.0",
4 | "dependencies": {}
5 | }
--------------------------------------------------------------------------------
/node-support/stub.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bundless",
3 | "version": "0.0.82",
4 | "description": "Experimental bundle-free dependency loader",
5 | "main": "./dist/src/index.js",
6 | "typings": "./dist/src/index.d.ts",
7 | "author": "Jiri Tobisek (https://github.com/tobich)",
8 | "license": "SEE LICENSE IN LICENSE.md",
9 | "private": false,
10 | "directories": {
11 | "test": "test"
12 | },
13 | "scripts": {
14 | "clean": "rimraf dist && mkdir dist",
15 | "pretest": "npm run build",
16 | "test": "npm run test:e2e && npm run test:integration && npm run test:unit",
17 | "test:unit": "mocha --reporter mocha-env-reporter ./dist/test/unit",
18 | "test:integration": "mocha --reporter mocha-env-reporter ./dist/test/integration",
19 | "test:e2e": "mocha --reporter mocha-env-reporter dist/test/e2e/e2e.spec.js",
20 | "build": "npm run clean && tsc",
21 | "prestart": "npm run build",
22 | "start": "node dist/src/index.js",
23 | "install": "cd node-libs && npm install"
24 | },
25 | "repository": {
26 | "type": "git",
27 | "url": "https://github.com/wix/bundless"
28 | },
29 | "bugs": {
30 | "url": "https://github.com/wix/bundless/issues"
31 | },
32 | "homepage": "https://github.com/wix/bundless",
33 | "devDependencies": {
34 | "@types/chai": "4.0.0",
35 | "@types/chai-as-promised": "0.0.31",
36 | "@types/fs-extra": "3.0.3",
37 | "@types/karma": "0.13.35",
38 | "@types/mocha": "2.2.41",
39 | "@types/tmp": "0.0.33",
40 | "bluebird": "3.5.0",
41 | "chai": "4.0.2",
42 | "chai-as-promised": "6.0.0",
43 | "express": "4.15.3",
44 | "fs-extra": "3.0.1",
45 | "karma": "1.7.0",
46 | "karma-chrome-launcher": "2.1.1",
47 | "karma-env-reporter": "1.0.13",
48 | "mocha": "3.4.2",
49 | "mocha-env-reporter": "2.0.4",
50 | "mocha-loader": "1.1.1",
51 | "portfinder": "1.0.13",
52 | "rimraf": "2.6.1",
53 | "source-map-support": "0.4.15",
54 | "tmp": "0.0.31",
55 | "typescript": "2.3.4"
56 | },
57 | "dependencies": {
58 | "@types/lodash": "4.14.65",
59 | "@types/node": "7.0.29",
60 | "@types/semver": "5.3.31",
61 | "@types/systemjs": "0.20.2",
62 | "lodash": "4.17.4",
63 | "semver": "5.3.0",
64 | "systemjs": "0.19.43"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/sample-server/express.ts:
--------------------------------------------------------------------------------
1 | import {Topology} from "../src/types";
2 | import path = require('path');
3 | import * as bundless from '../src';
4 | import {defTopology as defaultTopology} from '../src/defaults';
5 | import _ = require('lodash');
6 | import {Router} from "express";
7 | import express = require('express');
8 |
9 | function normalize(route: string): string {
10 | return route.replace(/[$]/g, () => '[$]');
11 | }
12 |
13 | export default function createExpressRouter(topologyOverrides: Topology): Router {
14 | const topology: Topology = _.merge({}, defaultTopology, topologyOverrides);
15 | const script = bundless.generateBootstrapScript(topology);
16 | const app: Router = express();
17 | app.use(normalize(topology.libMount), express.static(path.resolve(topology.rootDir, 'node_modules')));
18 | app.use(normalize(topology.srcMount), express.static(path.resolve(topology.rootDir, topology.srcDir)));
19 | app.use(normalize(topology.nodeMount), express.static(path.resolve(bundless.nodeRoot, 'node_modules')));
20 | app.get(normalize('/$bundless'), (req, res) => res.end(script));
21 | return app;
22 | }
--------------------------------------------------------------------------------
/src/api.ts:
--------------------------------------------------------------------------------
1 | import * as _hooks from './client/system-hooks';
2 | import * as _locator from './client/locator';
3 | export const hooks = _hooks;
4 | export const locator = _locator;
5 |
6 | export * from "./types";
7 | export * from "./project-mapper";
8 |
9 | export function hookSystemJs(systemJs:Object, baseURL:string, projectMap:Object, log?:(...args:string[])=>void, breakpointAt?:string, noJSExtension?:RegExp):void{
10 | systemJs['normalize'] = hooks.normalize(systemJs['normalize'].bind(systemJs), baseURL, locator, projectMap, log, breakpointAt, noJSExtension);
11 | systemJs['translate'] = hooks.translate(systemJs['translate'].bind(systemJs));
12 | }
--------------------------------------------------------------------------------
/src/client/loader-bootstrap.ts:
--------------------------------------------------------------------------------
1 | declare const systemHooks;
2 | declare const locator;
3 | declare const projectMap;
4 |
5 | const logginOn = !!window.location.search.match(/[?&]log=true/);
6 | const breakpointMatch = window.location.search.match(/[?&]bp=([^&]+)/);
7 | const breakpointAt = breakpointMatch ? breakpointMatch[1] : null;
8 | const log = logginOn ? console.log.bind(console, 'client >') : (...args:string[]) => {};
9 |
10 | const origNormalize = System['normalize'].bind(System);
11 | const origTranslate = System['translate'].bind(System);
12 |
13 | System['normalize'] = systemHooks.normalize(origNormalize, System.baseURL, locator, projectMap, log, breakpointAt);
14 |
15 | System['translate'] = systemHooks.translate(origTranslate);
16 |
17 |
18 | window['process'] = window['process'] || { env: {}, argv: [] };
--------------------------------------------------------------------------------
/src/client/locator.ts:
--------------------------------------------------------------------------------
1 | import {ProjectMap, PackageRec} from "./../project-mapper";
2 |
3 | function getExt(fileName: string): string {
4 | const slashIndex = fileName.lastIndexOf('/');
5 | const dotIndex = fileName.lastIndexOf('.');
6 | return (dotIndex > slashIndex) ? fileName.slice(dotIndex) : '';
7 | }
8 |
9 | function normalizeTail(name: string, ignorePattern:RegExp): string {
10 | if (ignorePattern && name.match(ignorePattern)){
11 | return name;
12 | }
13 | const ext = getExt(name);
14 | if(ext === '.js' || ext === '.json' || ext === '.') {
15 | return name;
16 | } else {
17 | return name + '.js';
18 | }
19 | }
20 |
21 | function remapFile(source: ParsedSource, rec: PackageRec): ParsedSource {
22 | const remapObj = rec.r;
23 |
24 | function tryKey(key: string): ParsedSource {
25 | if(key in remapObj) {
26 | const value = remapObj[key];
27 | if(typeof value === 'boolean') {
28 | if(value === false) {
29 | throw new Error(`Target ${key} explicitly forbidden in package.json`);
30 | }
31 | } else {
32 | return parseSource(value);
33 | }
34 | } else {
35 | return null;
36 | }
37 |
38 | }
39 |
40 | if(remapObj) {
41 | const localPath = source.localPath.slice(0, 2) === './' ? source.localPath : './' + source.localPath;
42 |
43 | return tryKey(source.pkg) ||
44 | tryKey(source.pkg + '/' + stripJsExt(source.localPath)) ||
45 | tryKey(localPath) ||
46 | tryKey(stripJsExt(localPath));
47 | }
48 | return null;
49 | }
50 |
51 | function resolveAsPackage(projectMap: ProjectMap, baseUrl: string, parsedSource: ParsedSource, parsedParent: ParsedUrl, noJSExtension?:RegExp): string {
52 |
53 | function resolvePackageName(parsedSource: ParsedSource, parsedParent: ParsedSource): ParsedSource {
54 | const parentPkg = parsedParent && projectMap.packages[parsedParent.pkg];
55 | if(parentPkg) {
56 | return remapFile(parsedSource, parentPkg) || parsedSource;
57 | } else {
58 | return parsedSource;
59 | }
60 | }
61 |
62 | const source: ParsedSource = resolvePackageName(parsedSource, parsedParent);
63 |
64 | if(source.pkg) {
65 | if(source.pkg in projectMap.packages) {
66 | const { p: moduleSource, m: modulePath } = projectMap.packages[source.pkg];
67 | const tail = parsedSource.localPath || modulePath;
68 | return joinUrl(baseUrl, moduleSource + '/' + normalizeTail(tail, noJSExtension));
69 | } else {
70 | return null;
71 | }
72 | } else {
73 | return source.localPath;
74 | }
75 |
76 |
77 | }
78 |
79 | function isDefaultIndexDir(projectMap: ProjectMap, filePath: string): boolean {
80 | const key = filePath.charAt(filePath.length - 1) === '/'
81 | ? filePath.slice(0, -1) + '.js'
82 | : filePath;
83 | return projectMap.dirs.indexOf(key) > -1;
84 | }
85 |
86 | function stripJsExt(pathName: string): string {
87 | if(getExt(pathName) === '.js') {
88 | return pathName.slice(0,-3);
89 | } else {
90 | return pathName;
91 | }
92 | }
93 |
94 |
95 | export interface ParsedSource {
96 | pkg: string;
97 | localPath: string;
98 | ext: string;
99 | }
100 |
101 | export interface ParsedUrl extends ParsedSource {
102 | pkgPath: string;
103 | }
104 |
105 |
106 | export function parseSource(source: string): ParsedSource {
107 | const segments = source.split('/');
108 | if(segments[0] === '.' || segments[0] === '..') {
109 | return {
110 | pkg: '',
111 | localPath: source,
112 | ext: getExt(source)
113 | }
114 | } else {
115 | return {
116 | pkg: segments[0],
117 | localPath: segments.slice(1).join('/'),
118 | ext: getExt(source)
119 | }
120 | }
121 | }
122 |
123 | export function parseUrl(url: string, baseUrl: string, libMount: string): ParsedUrl {
124 | const ext: string = getExt(url);
125 | const urlPath: string = url.slice(baseUrl.length);
126 | const segments = urlPath.split('/');
127 | const libSegments = libMount.split('/');
128 | const startIndex = libSegments.every((segment, index) => segment === segments[index])
129 | ? libSegments.length
130 | : -1;
131 | if(startIndex > -1) {
132 | const pkgIndex = segments
133 | .reduce((acc: number, it: string, index: number, list: string[]) => {
134 | return (index > startIndex && list[index-1] === 'node_modules') ? index : acc;
135 | }, startIndex);
136 | return {
137 | pkg: segments[pkgIndex],
138 | pkgPath: segments.slice(0,pkgIndex+1).join('/'),
139 | localPath: segments.slice(pkgIndex+1).join('/'),
140 | ext
141 | }
142 | } else {
143 | return {
144 | pkg: '',
145 | pkgPath: '',
146 | localPath: urlPath,
147 | ext
148 | };
149 | }
150 | }
151 |
152 | function fixDuplicateSlases(name: string): string {
153 | return name.replace(/\/+/g, (found: string, index: number, str: string) => {
154 | if(index === 5 && str.slice(0, index) === 'http:') {
155 | return found;
156 | } else {
157 | return '/';
158 | }
159 | });
160 | }
161 |
162 | export function joinUrl(baseUrl: string, ...paths: string[]): string {
163 | let result = baseUrl;
164 | paths.forEach(path => {
165 | if(result.charAt(result.length-1) !== '/') {
166 | result += '/';
167 | }
168 | if(path.slice(0,2) === './') {
169 | result += path.slice(2);
170 | } else if(path.charAt(0) === '/') {
171 | result += path.slice(1);
172 | } else {
173 | result += path;
174 | }
175 | });
176 | return result;
177 | }
178 |
179 | export function preProcess(projectMap: ProjectMap, baseUrl, name: string, parentName?: string, parentAddress?: string, noJSExtension?:RegExp): string {
180 | const normalizedNamed = fixDuplicateSlases(name);
181 | const parsedSource: ParsedSource = parseSource(normalizedNamed);
182 | if(!parsedSource.pkg) {
183 | return normalizeTail(normalizedNamed, noJSExtension);
184 | } else {
185 | const parsedParent: ParsedUrl = parentName ? parseUrl(parentName, baseUrl, projectMap.libMount) : null;
186 | const pkgMainFilePath = resolveAsPackage(projectMap, baseUrl, parsedSource, parsedParent, noJSExtension);
187 | if(pkgMainFilePath) {
188 | return normalizeTail(pkgMainFilePath, noJSExtension);
189 | } else {
190 | return normalizeTail(normalizedNamed, noJSExtension);
191 | }
192 | }
193 | }
194 |
195 | export function applyFileRemapping(projectMap: ProjectMap, url: ParsedUrl): string {
196 | const origPath = joinUrl(url.pkgPath, url.localPath);
197 | if(url.pkg && url.pkg in projectMap.packages) {
198 | const pkgRec = projectMap.packages[url.pkg];
199 | if(url.localPath === '') {
200 | const remappedMainFile = remapFile({
201 | pkg: '',
202 | localPath: pkgRec.m,
203 | ext: getExt(url.localPath)
204 | }, pkgRec);
205 | if(remappedMainFile) {
206 | return joinUrl(url.pkgPath, remappedMainFile.localPath);
207 | } else {
208 | return joinUrl(url.pkgPath, pkgRec.m);
209 | }
210 | } else {
211 | const remappedSource: ParsedSource = remapFile(url, pkgRec);
212 | if(remappedSource) {
213 | return joinUrl(url.pkgPath, remappedSource.localPath.slice(2));
214 | } else {
215 | return origPath;
216 | }
217 | }
218 | } else {
219 | return origPath;
220 | }
221 | }
222 |
223 | export function postProcess(projectMap: ProjectMap, baseUrl: string, resolvedName: string, noJSExtension?:RegExp): string {
224 | const filePath: string = resolvedName.slice(baseUrl.length);
225 | if(isDefaultIndexDir(projectMap, '/' + filePath)) {
226 | return joinUrl(baseUrl, stripJsExt(filePath), 'index.js');
227 | } else {
228 | const url: ParsedUrl = parseUrl(resolvedName, baseUrl, projectMap.libMount);
229 | const remappedFile: string = normalizeTail(applyFileRemapping(projectMap, url), noJSExtension);
230 | return joinUrl(baseUrl, remappedFile);
231 | }
232 | }
--------------------------------------------------------------------------------
/src/client/system-hooks.ts:
--------------------------------------------------------------------------------
1 |
2 | export function normalize(origNormalize, baseURL:string, locator, projectMap:Object, log = (...args:string[]) => {}, breakpointAt?:string, noJSExtension?:RegExp){
3 | return function bundlessNormalize(name: string, parentName: string, parentAddress: string) {
4 | const newName = locator.preProcess(projectMap, baseURL, name, parentName, parentAddress, noJSExtension);
5 | log(`preProcess() ${name} -> ${newName}`);
6 | return origNormalize(newName, parentName, parentAddress)
7 | .then(resolvedName => {
8 | const result = locator.postProcess(projectMap, baseURL, resolvedName, noJSExtension);
9 | log(`postProcess() ${name}: ${resolvedName} -> ${result}`);
10 | if(result === breakpointAt) {
11 | const params = { name, parentName, parentAddress, newName, resolvedName, result }; /* tslint:disable */
12 | debugger; /* tslint:enable */
13 | }
14 | return result;
15 | });
16 | };
17 | }
18 |
19 | export function translate(origTranslate) {
20 | return function bundlessTranslate(load) {
21 | if(load.name.slice(-5) === '.json') {
22 | return 'module.exports = ' + load.source;
23 | } else {
24 | return origTranslate(load);
25 | }
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/src/defaults.ts:
--------------------------------------------------------------------------------
1 | import _ = require('lodash');
2 | import {collectDirInfo} from "./dir-structure";
3 | import path = require('path');
4 | import {Topology, ProjectMapperOptions, ProjectInfo, BootstrapScriptOptions} from "./types";
5 | import * as nodeSupport from './node-support';
6 |
7 | export function generateProjectInfo(bootstrapOptions:BootstrapScriptOptions):ProjectInfo {
8 | const actualOptions:ProjectMapperOptions = _.merge({}, defProjectMapperOptions, bootstrapOptions.mapper);
9 | const srcDir = path.resolve(bootstrapOptions.rootDir, bootstrapOptions.srcDir);
10 | const libDir = path.resolve(bootstrapOptions.rootDir, 'node_modules');
11 | const excludeFromSrc: string[] = (path.resolve(bootstrapOptions.rootDir) === srcDir)
12 | ? [libDir]
13 | : [];
14 | const projectInfo:ProjectInfo = {
15 | rootDir: bootstrapOptions.rootDir,
16 | srcDir: bootstrapOptions.srcDir,
17 | srcMount: bootstrapOptions.srcMount,
18 | libMount: bootstrapOptions.libMount,
19 | nodeMount: bootstrapOptions.nodeMount,
20 | srcInfo: actualOptions.collector(srcDir, excludeFromSrc),
21 | libInfo: actualOptions.collector(libDir),
22 | nodeLibInfo: actualOptions.nodeLibs? actualOptions.collector(path.join(nodeSupport.rootDir, 'node_modules')) : undefined
23 | };
24 | return projectInfo;
25 | }
26 |
27 | export const defTopology: Topology = {
28 | rootDir: process.cwd(),
29 | srcDir: 'src',
30 | srcMount: '/modules',
31 | libMount: '/lib',
32 | nodeMount: '/$node'
33 | };
34 |
35 | export const defProjectMapperOptions: ProjectMapperOptions = {
36 | nodeLibs: true,
37 | collector: collectDirInfo
38 | };
39 |
40 | export const defBootstrapScriptOptions: BootstrapScriptOptions = _.merge({}, defTopology, {
41 | exportSymbol: '$bundless',
42 | mapper: defProjectMapperOptions
43 | });
--------------------------------------------------------------------------------
/src/dir-structure.ts:
--------------------------------------------------------------------------------
1 | import fs = require('fs');
2 | import path = require('path');
3 | import os = require('os');
4 | import {DirInfo, DirInfoDict} from "./types";
5 | import _ = require('lodash');
6 |
7 |
8 | const relevantFiles = ['package.json', 'bower.json', 'index.js'];
9 |
10 | function normalizePath(pathName: string): string {
11 | return os.platform() === 'win32'
12 | ? pathName.replace(/\\/g, () => '/')
13 | : pathName;
14 | }
15 |
16 | function collect(rootDir: string, parent: DirInfo = null, exclude: string[] = []): DirInfo {
17 | let stat: fs.Stats;
18 | const name = path.basename(rootDir);
19 | const parentPath = parent ? parent.path : '';
20 | const item: DirInfo = {
21 | name,
22 | path: normalizePath(path.join(parentPath, name)),
23 | parent
24 | };
25 | try {
26 | stat = fs.statSync(rootDir);
27 | } catch (err) {
28 | return null;
29 | }
30 |
31 | if(stat.isDirectory()) {
32 | const list = fs.readdirSync(rootDir);
33 | item.children = list
34 | .reduce((acc, name) => {
35 | const childPath = path.join(rootDir, name);
36 | if(!_.includes(exclude, childPath)) {
37 | const childItem = collect(childPath, item, exclude);
38 | if(childItem) {
39 | acc[name] = childItem;
40 | }
41 | }
42 | return acc;
43 | }, {});
44 | return item;
45 | } else {
46 | if(_.includes(relevantFiles, name)) {
47 | if(name === 'package.json' || name === 'bower.json') {
48 | try {
49 | item.content = JSON.parse(
50 | fs.readFileSync(rootDir).toString()
51 | );
52 | } catch (err) {
53 | item.content = {};
54 | }
55 | }
56 | return item;
57 | } else {
58 | return null;
59 | }
60 | }
61 | }
62 |
63 | export function collectDirInfo(rootDir: string, exclude: string[] = []): DirInfo {
64 | return collect(rootDir, null, exclude);
65 | }
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import fs = require('fs');
2 | import path = require('path');
3 | import _ = require('lodash');
4 | import {defBootstrapScriptOptions, generateProjectInfo} from "./defaults";
5 | import * as nodeSupport from './node-support';
6 | import {BootstrapScriptOptions, ProjectInfo, ProjectMapperOptions, ProjectMap, getProjectMap} from "./api";
7 |
8 | function readModule(moduleId:string):string {
9 | return fs.readFileSync(path.resolve(__dirname, moduleId)).toString();
10 | }
11 | function loadModule(moduleId:string){
12 | return `(function () {
13 | var exports = {};
14 | ${readModule(moduleId)}
15 | return exports;
16 | })();`;
17 | }
18 | export {rootDir as nodeRoot} from './node-support';
19 |
20 | export {defBootstrapScriptOptions as defaultOptions};
21 |
22 | export function generateBootstrapScript(options: BootstrapScriptOptions = {}, systemConfigOverrides:Object = {}): string {
23 | const bootstrapOptions: BootstrapScriptOptions = _.merge({}, defBootstrapScriptOptions, options);
24 | const defaultSystemConfig = {
25 | defaultJSExtensions: false,
26 | meta: {
27 | [bootstrapOptions.nodeMount.slice(1) + '/*']: {
28 | deps: [nodeSupport.globals]
29 | },
30 | '*': {
31 | format: 'cjs'
32 | }
33 | }
34 | };
35 |
36 | const systemConfig = JSON.stringify(
37 | _.merge({}, defaultSystemConfig, systemConfigOverrides)
38 | );
39 |
40 | const projectMap:ProjectMap = getProjectMap(generateProjectInfo(bootstrapOptions));
41 | const loaderBootstrap = readModule('./client/loader-bootstrap.js');
42 |
43 | return `
44 | (function () {
45 | var bootstrap = function (System) {
46 | var systemHooks = ${loadModule('./client/system-hooks.js')};
47 | var locator = ${loadModule('./client/locator.js')};
48 | var projectMap = ${JSON.stringify(projectMap)};
49 | System.config(${systemConfig});
50 | ${loaderBootstrap};
51 | return projectMap;
52 | };
53 | if(typeof module === 'undefined') {
54 | window["${bootstrapOptions.exportSymbol}"] = bootstrap;
55 | } else {
56 | module.exports = bootstrap;
57 | }
58 | })()
59 | `;
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/src/logger.ts:
--------------------------------------------------------------------------------
1 | export function log(...args): any {
2 | if(process.env.BUNDLESS_DEBUG) {
3 | console.log.apply(console, args);
4 | }
5 | return args[0];
6 | }
7 |
--------------------------------------------------------------------------------
/src/node-support.ts:
--------------------------------------------------------------------------------
1 | import {PackageRec, PackageDict} from "./project-mapper";
2 | import fs = require('fs');
3 | import path = require('path');
4 |
5 | export const rootDir: string = path.resolve(__dirname, '../../node-libs');
6 |
7 | export const supportedLibs = [
8 | "assert",
9 | "buffer",
10 | "child_process",
11 | "cluster",
12 | "console",
13 | "constants",
14 | "crypto",
15 | "dgram",
16 | "dns",
17 | "domain",
18 | "events",
19 | "fs",
20 | "http",
21 | "https",
22 | "module",
23 | "net",
24 | "os",
25 | "path",
26 | "process",
27 | "punycode",
28 | "querystring",
29 | "readline",
30 | "repl",
31 | "stream",
32 | "string_decoder",
33 | "sys",
34 | "timers",
35 | "tls",
36 | "tty",
37 | "url",
38 | "util",
39 | "vm",
40 | "zlib"
41 | ];
42 |
43 | export type AliasValue = string | ((dict: PackageDict) => PackageRec);
44 | export type AliasDict = { [alias: string]: AliasValue }
45 | export const aliases: AliasDict = {
46 | "console": "console-browserify",
47 | "constants": "constants-browserify",
48 | // "crypto": "crypto-browserify",
49 | "domain": "domain-browser",
50 | "http": "http-browserify",
51 | "https": "https-browserify",
52 | "os": "os-browserify",
53 | "path": "path-browserify",
54 | "querystring": "querystring-es3",
55 | "stream": "stream-browserify",
56 | "sys": "util",
57 | "timers": "timers-browserify",
58 | "tty": "tty-browserify",
59 | "util": "util",
60 | "vm": "vm-browserify",
61 | "zlib": "browserify-zlib",
62 |
63 | // stubs:
64 | "child_process": null,
65 | "cluster": null,
66 | "crypto": null,
67 | "dgram": null,
68 | "dns": null,
69 | "fs": null,
70 | "module": null,
71 | "net": null,
72 | "readline": null,
73 | "repl": null,
74 | "tls": null,
75 |
76 | // Dynamic aliases
77 | "_stream_transform" : dict => ({ p: dict['readable-stream'].p, m: 'transform.js' })
78 | };
79 |
80 | export const stubPath = 'node-support/stub.js';
81 | export const globals = 'node-support/globals.js';
82 |
83 |
--------------------------------------------------------------------------------
/src/project-mapper.ts:
--------------------------------------------------------------------------------
1 | import {Topology, ProjectInfo, DirInfo} from "./types";
2 | import {defProjectMapperOptions} from "./defaults";
3 | import path = require('path');
4 | import semver = require('semver');
5 | import * as nodeSupport from "./node-support";
6 | import _ = require('lodash');
7 |
8 | function getPackageVersion(pkg: DirInfo): string {
9 | return pkg.children['package.json']['content']['version'] || '0.0.0';
10 | }
11 |
12 | function resolveFileRemapping(pkg: DirInfo): FileRemapping {
13 | const browserProp = pkg.children['package.json']['content']['browser'];
14 | if(browserProp && typeof browserProp === 'object') {
15 | return browserProp;
16 | } else {
17 | return null;
18 | }
19 | }
20 |
21 | export function traverseDirInfo(root: DirInfo, visitor: (node: DirInfo) => void): void {
22 | if(root) {
23 | visitor.call(null, root);
24 | if(root.children) {
25 | for(let childName in root.children) {
26 | traverseDirInfo(root.children[childName], visitor);
27 | }
28 | }
29 | }
30 | }
31 |
32 | function calcDepth(dirInfo: DirInfo, currentDepth: number = 0): number {
33 | if(dirInfo.parent) {
34 | return calcDepth(dirInfo.parent, currentDepth+1);
35 | } else {
36 | return currentDepth;
37 | }
38 | }
39 |
40 | function resolvePkgVersions(newPkg: DirInfo, existingPkg: DirInfo): DirInfo {
41 | const newVersion = getPackageVersion(newPkg);
42 | const existingVersion = getPackageVersion(existingPkg);
43 | if(semver.eq(newVersion, existingVersion)) {
44 | if(calcDepth(existingPkg) > calcDepth(newPkg)) {
45 | return newPkg;
46 | } else {
47 | return existingPkg;
48 | }
49 | } else if(semver.gt(newVersion, existingVersion)) {
50 | return newPkg;
51 | } else {
52 | return existingPkg;
53 | }
54 | }
55 |
56 | /** @deprecated */
57 | function resolveBowerMainFile(dirInfo: DirInfo): string {
58 | const result = _.property(['children', 'bower.json', 'content', 'main'])(dirInfo);
59 | if(typeof result === 'string') {
60 | return result;
61 | } else if(typeof result === 'object') {
62 | return result[0];
63 | }
64 | }
65 |
66 | function resolveJspmMainFile(dirInfo: DirInfo): string {
67 | return _.property(['children', 'package.json', 'content', 'jspm', 'main'])(dirInfo);
68 | }
69 |
70 | function resolvePackageJsonMainFile(dirInfo: DirInfo): string {
71 | const browserProp = _.property(['children', 'package.json', 'content', 'browser'])(dirInfo);
72 | if(typeof browserProp === 'string') {
73 | return browserProp;
74 | } else {
75 | return _.property(['children', 'package.json', 'content', 'main'])(dirInfo);
76 | }
77 | }
78 |
79 | function resolveMainPkgFile(dirInfo: DirInfo): string {
80 | return resolveJspmMainFile(dirInfo) ||
81 | resolvePackageJsonMainFile(dirInfo) ||
82 | 'index.js';
83 | }
84 |
85 | interface PackageDictOptions {
86 | lookupBrowserJs?: boolean
87 | }
88 |
89 | function buildPkgDict(dirInfo: DirInfo, libMount: string, options: PackageDictOptions = {}): PackageDict {
90 | const pkgDict: { [pkgName: string]: DirInfo } = {};
91 | traverseDirInfo(dirInfo, (node: DirInfo) => {
92 | if(node.name === 'package.json') {
93 | const pkg: DirInfo = node.parent;
94 | const pkgName = pkg.name;
95 | const existingVersion = pkgDict[pkgName];
96 | if(existingVersion) {
97 | pkgDict[pkgName] = resolvePkgVersions(pkg, existingVersion);
98 | } else {
99 | pkgDict[pkgName] = pkg;
100 | }
101 | }
102 | });
103 |
104 | const finalDict: PackageDict = {};
105 | for(let pkgName in pkgDict) {
106 | const pkg: DirInfo = pkgDict[pkgName];
107 | const pkgPath = libMount + pkg.path.slice(dirInfo.path.length);
108 | const mainFilePath = resolveMainPkgFile(pkg);
109 | const remapping: FileRemapping = resolveFileRemapping(pkg);
110 | const pkgRec: PackageRec = { p: pkgPath, m: mainFilePath };
111 | if(remapping) {
112 | pkgRec.r = remapping;
113 | }
114 | finalDict[pkgName] = pkgRec;
115 | }
116 |
117 | return finalDict;
118 | }
119 |
120 | function joinUrls(url1: string, url2: string): string {
121 | if(_.last(url1) === '/' && _.first(url2) === '/') {
122 | return url1 + url2.slice(1);
123 | } else {
124 | return url1 + url2;
125 | }
126 | }
127 |
128 | function collectIndexDirs(root: DirInfo, prefix: string): string[] {
129 | const list: string[] = [];
130 | traverseDirInfo(root, (node: DirInfo) => {
131 | if(node.name === 'index.js' && !('package.json' in node.parent.children)) {
132 | const url = joinUrls(prefix, node.parent.path.slice(root.path.length) + '.js');
133 | list.push(url);
134 | }
135 | });
136 | return list;
137 | }
138 |
139 | // These properties have short names because we're trying to make the project map as small as possible
140 |
141 | export type FileRemapping = { [fileName: string]: (string | boolean) };
142 |
143 | export type PackageRec = {
144 | p: string; // package path
145 | m: string; // main file local path
146 | r?: FileRemapping;
147 | };
148 |
149 | export type PackageDict = { [pkgName: string]: PackageRec };
150 |
151 | export interface ProjectMap {
152 | libMount: string;
153 | packages: PackageDict;
154 | dirs: string[];
155 | }
156 |
157 |
158 | function getNodeLibMap(nodeMount: string, nodeLibStructure: DirInfo): ProjectMap {
159 | const packages: PackageDict = buildPkgDict(nodeLibStructure, nodeMount, { lookupBrowserJs: true });
160 | _.forEach(nodeSupport.aliases, (aliasValue: nodeSupport.AliasValue, alias: string) => {
161 | if(typeof aliasValue === 'string') {
162 | packages[alias] = packages[aliasValue];
163 | } else if(aliasValue === null) {
164 | packages[alias] = { p: nodeMount, m: nodeSupport.stubPath };
165 | } else {
166 | packages[alias] = aliasValue(packages);
167 | }
168 | });
169 |
170 | const dirs = collectIndexDirs(nodeLibStructure, nodeMount);
171 | return { libMount: '', packages, dirs };
172 | }
173 |
174 | function mergeProjectMaps(map1: ProjectMap, map2: ProjectMap): ProjectMap {
175 | return {
176 | libMount: map1.libMount,
177 | packages: _.assign({}, map1.packages, map2.packages),
178 | dirs: map1.dirs.concat(map2.dirs)
179 | };
180 | }
181 |
182 | export function getProjectMap(projInfo: ProjectInfo): ProjectMap {
183 |
184 | const packages: PackageDict = buildPkgDict(projInfo.libInfo, projInfo.libMount);
185 | const dirs: string[] = []
186 | .concat(collectIndexDirs(projInfo.srcInfo, projInfo.srcMount))
187 | .concat(collectIndexDirs(projInfo.libInfo, projInfo.libMount));
188 |
189 | const projectMap: ProjectMap = {
190 | libMount: projInfo.libMount.slice(1),
191 | packages,
192 | dirs
193 | };
194 |
195 | if(projInfo.nodeLibInfo) {
196 | return mergeProjectMaps(projectMap, getNodeLibMap(projInfo.nodeMount, projInfo.nodeLibInfo));
197 | } else {
198 | return projectMap;
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export type DirInfoDict = { [name: string]: DirInfo };
2 |
3 | export interface DirInfo {
4 | name: string;
5 | path: string;
6 | children?: DirInfoDict;
7 | content?: Object;
8 | parent: DirInfo;
9 | }
10 |
11 | export interface ProjectInfo extends Topology{
12 | srcInfo:DirInfo;
13 | libInfo:DirInfo;
14 | nodeLibInfo?:DirInfo;
15 | }
16 |
17 | export interface DirInfoCollector {
18 | (rootDir: string, exclude?: string[]): DirInfo;
19 | }
20 |
21 | export interface Topology {
22 | rootDir?: string;
23 | srcDir?: string;
24 | srcMount?: string;
25 | libMount?: string;
26 | nodeMount?: string;
27 | }
28 |
29 | export interface ProjectMapperOptions {
30 | nodeLibs?: boolean;
31 | collector?: DirInfoCollector;
32 | }
33 |
34 | export interface BootstrapScriptOptions extends Topology {
35 | exportSymbol?: string;
36 | mapper?: ProjectMapperOptions;
37 | }
38 |
--------------------------------------------------------------------------------
/src/url-resolver.ts:
--------------------------------------------------------------------------------
1 | import {Topology} from "./types";
2 | import path = require('path');
3 | import * as nodeSupport from './node-support';
4 |
5 | export function testMountPoint(mountPoint: string, fullUrl:string): string {
6 | const mountPointLength = mountPoint.length;
7 | if(fullUrl.slice(0, mountPointLength) === mountPoint && fullUrl.charAt(mountPointLength) === '/') {
8 | return fullUrl.slice(mountPointLength + 1);
9 | } else {
10 | return null;
11 | }
12 | }
13 |
14 |
15 | export function resolveUrlToFile(topology: Topology, url: string): string {
16 | const prefixIndex = url.indexOf('/', 1);
17 | const prefix = prefixIndex === -1 ? '/' : url.slice(0, prefixIndex);
18 | const filePath = prefixIndex === -1 ? url.slice(1) : url.slice(prefixIndex+1);
19 | if(prefix === topology.srcMount) {
20 | return path.join(topology.rootDir, topology.srcDir, filePath);
21 | } else if(prefix === topology.libMount) {
22 | return path.join(topology.rootDir, 'node_modules', filePath);
23 | } else if(prefix === topology.nodeMount) {
24 | return path.join(nodeSupport.rootDir, 'node_modules', filePath);
25 | } else {
26 | return null;
27 | }
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/test-kit/karma-server.ts:
--------------------------------------------------------------------------------
1 | import * as karma from 'karma';
2 | import Promise = require("bluebird");
3 | import {findPort} from "./port";
4 |
5 |
6 | export function startKarmaServer(host: string, port: number, basePath: string, mainModule: string): Promise {
7 | return findPort(9876)
8 | .then(karmaPort => {
9 | return new Promise((resolve, reject) => {
10 |
11 | const karmaServer = new karma.Server({
12 | port: karmaPort,
13 | configFile: process.cwd() + '/karma.conf.js',
14 | singleRun: true,
15 | browserNoActivityTimeout: 100000,
16 | client: {
17 | baseURL: `http://${host}:${port}${basePath}`,
18 | mainModule
19 | }
20 | }, exitCode => {
21 | console.log(`Karma server finished with exit code ${exitCode}`);
22 | });
23 | karmaServer.on('run_complete', (browsers, result) => {
24 | if(result.exitCode === 0) {
25 | resolve(!result.error);
26 | } else {
27 | reject(`Karma exited with code ${result.exitCode}`);
28 | }
29 | });
30 | karmaServer.start();
31 | });
32 | })
33 |
34 | }
35 |
36 |
37 |
--------------------------------------------------------------------------------
/test-kit/port.ts:
--------------------------------------------------------------------------------
1 | import Promise = require('bluebird');
2 | const portfinder = require("portfinder");
3 |
4 | export function findPort(startFrom: number): Promise {
5 | portfinder.basePort = startFrom;
6 | return Promise.promisify(portfinder.getPort)();
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/test-kit/project-driver.ts:
--------------------------------------------------------------------------------
1 | import fs = require('fs-extra');
2 | import path = require('path');
3 | import _ = require('lodash');
4 |
5 | export class PackageBuilder {
6 | constructor(private name: string, private rootDir: string, version?:string) {
7 | const packageJson = {
8 | name
9 | };
10 | if(version) {
11 | packageJson['version'] = version;
12 | }
13 | this.writeFile('package.json', packageJson);
14 | }
15 |
16 | addFile(fileName: string, content: Object | string = ''): PackageBuilder {
17 | if(typeof content === 'object') {
18 | this.writeFile(fileName, JSON.stringify(content, null, 4));
19 | } else {
20 | this.writeFile(fileName, content);
21 | }
22 | return this;
23 | }
24 |
25 | addMainFileToPackageJson(fileName: string, property: string): PackageBuilder {
26 | const packageJson: Object = this.readJSON('package.json');
27 | packageJson[property] = fileName;
28 | this.writeFile('package.json', packageJson);
29 | return this;
30 | }
31 |
32 | addMainFile(fileName: string, content: string = ''): PackageBuilder {
33 | this.writeFile(fileName, content);
34 | return this.addMainFileToPackageJson(fileName, 'main');
35 | }
36 |
37 | addBrowserMainFile(fileName: string, content: string = ''): PackageBuilder {
38 | this.writeFile(fileName, content);
39 | return this.addMainFileToPackageJson(fileName, 'browser');
40 | }
41 |
42 | addBowerMainFile(fileName: string, content: string = ''): PackageBuilder {
43 | this.writeFile('bower.json', { main: fileName });
44 | return this;
45 | }
46 |
47 | addJspmMainFile(fileName: string, content: string = ''): PackageBuilder {
48 | this.writeFile(fileName, content);
49 | const packageJson: Object = this.readJSON('package.json');
50 | packageJson["jspm"] = { "main": fileName };
51 | this.writeFile('package.json', packageJson);
52 | return this;
53 | }
54 |
55 | addPackage(name: string, version?: string): PackageBuilder {
56 | const newPath: string = path.resolve(this.rootDir, 'node_modules', name);
57 | return new PackageBuilder(name, newPath, version);
58 | }
59 |
60 | addToPackageJson(obj: Object): PackageBuilder {
61 | const packageJson: Object = this.readJSON('package.json');
62 | _.merge(packageJson, obj);
63 | this.writeFile('package.json', packageJson);
64 | return this;
65 | }
66 |
67 | getPath(): string {
68 | return this.rootDir;
69 | }
70 |
71 | dispose(): void {
72 | fs.removeSync(this.rootDir);
73 | }
74 |
75 | private writeFile(filePath: string, content: string | Object): void {
76 | const finalContent = typeof content === 'object' ? JSON.stringify(content, null, 4) : content;
77 | const fullPath = this.getFullName(filePath);
78 | fs.ensureFileSync(fullPath);
79 | fs.writeFileSync(fullPath, finalContent);
80 | }
81 |
82 | readFile(filePath: string): string {
83 | const fullPath = this.getFullName(filePath);
84 | return fs.readFileSync(fullPath).toString();
85 | }
86 |
87 | private readJSON(filePath: string): Object {
88 | return JSON.parse(this.readFile(filePath));
89 | }
90 |
91 | private getFullName(fileName: string): string {
92 | return path.resolve(this.rootDir, fileName);
93 | }
94 | }
95 |
96 | export default function project(rootDir: string): PackageBuilder {
97 | return new PackageBuilder('project-root', rootDir);
98 | }
99 |
--------------------------------------------------------------------------------
/test-kit/test-server.ts:
--------------------------------------------------------------------------------
1 | import {Topology} from "../src/types";
2 | import express = require('express');
3 | import {Application} from "express";
4 | import {Server} from "http";
5 | import Promise = require('bluebird');
6 | import {Request, Response} from "express";
7 | import bundlessExpress from "../sample-server/express";
8 |
9 | function log() {
10 | return (req:Request, res:Response, next:Function) => {
11 | res.on('finish', () => {
12 | console.log(req.method, req.originalUrl, '->', res.statusCode);
13 | });
14 | next();
15 | }
16 | }
17 |
18 | export interface StaticServerOptions {
19 | debug: boolean;
20 | }
21 |
22 | const defaultServerOptions: StaticServerOptions = {
23 | debug: false
24 | };
25 |
26 | export function startStaticServer(host: string, port: number, basePath: string, topologyOverrides: Topology, options: StaticServerOptions = defaultServerOptions): Promise {
27 | const app: Application = express();
28 | if(options.debug) {
29 | app.use(log());
30 | }
31 | app.use(basePath, bundlessExpress(topologyOverrides));
32 | return new Promise((resolve, reject) => {
33 | app.listen(port, host, function (err) {
34 | if(err) {
35 | reject(err);
36 | } else {
37 | resolve(this);
38 | }
39 | })
40 | });
41 | }
--------------------------------------------------------------------------------
/test/e2e/client-script.ts:
--------------------------------------------------------------------------------
1 | function loadScript(url) {
2 | return new Promise((resolve, reject) => {
3 | const element = document.createElement('script');
4 | element.src = url;
5 | element.addEventListener('load', () => {
6 | resolve();
7 | });
8 | element.addEventListener('error', () => {
9 | reject();
10 | });
11 | document.body.appendChild(element);
12 | });
13 | }
14 |
15 | const karma = window['__karma__'];
16 |
17 | function finish(result, errors) {
18 | karma.result({
19 | id: '',
20 | description: 'e2e',
21 | suite: [],
22 | success: result,
23 | skipped: null,
24 | time: 0,
25 | log: [],
26 | assertionErrors: errors
27 | });
28 | karma.complete();
29 | }
30 |
31 |
32 | const config = karma.config;
33 | karma.info({ total: 1 });
34 | karma.start = function () {
35 | loadScript(`${config.baseURL}$bundless`)
36 | .then(() => {
37 | System.config({ baseURL: config.baseURL });
38 | window['$bundless'](System);
39 | return System.import(config.mainModule)
40 | .catch(err => {
41 | karma.log('ERROR', [err.message]);
42 | finish(false, [err.message]);
43 | })
44 | .then(() => finish(true, []));
45 | });
46 | };
--------------------------------------------------------------------------------
/test/e2e/e2e.spec.ts:
--------------------------------------------------------------------------------
1 | import {PackageBuilder} from "../../test-kit/project-driver";
2 | import {setupProject} from "./project-fixtures";
3 | import {startStaticServer} from "../../test-kit/test-server";
4 | import {Topology} from "../../src/types";
5 | import Promise = require("bluebird");
6 | import * as http from "http";
7 | import {startKarmaServer} from "../../test-kit/karma-server";
8 | import {expect} from "chai";
9 | import {defTopology} from "../../src/defaults";
10 | import {findPort} from "../../test-kit/port";
11 |
12 | const host = 'localhost';
13 |
14 | describe('Bundless', function () {
15 | this.timeout(160000);
16 |
17 |
18 | ['/', '/complex/path/'].forEach((basePath) => {
19 | describe(`loads sample project with base '${basePath}'`, function () {
20 |
21 | let staticServer: http.Server;
22 |
23 | function runTest(topology: Topology) {
24 | const mainModule = topology.srcMount === '/' ? 'main.js' : `${topology.srcMount.slice(1)}/main.js`;
25 | const project: PackageBuilder = setupProject(topology.srcDir);
26 | topology.rootDir = project.getPath();
27 | return findPort(3000)
28 | .then(port => {
29 | return Promise.resolve()
30 | .then(() => startStaticServer(host, port, basePath, topology))
31 | .then(result => staticServer = result)
32 | .then(() => startKarmaServer(host, port, basePath, mainModule))
33 | .then(passed => expect(passed).to.equal(true, 'Expected all tests to pass'))
34 | .then(() => project.dispose());
35 | });
36 | }
37 |
38 |
39 | it('using default topology', function () {
40 | return runTest(defTopology);
41 | });
42 |
43 | it('using simple topology', function () {
44 | return runTest({
45 | srcDir: 'dist',
46 | srcMount: '/modules',
47 | libMount: '/node_modules',
48 | nodeMount: '/$node'
49 | });
50 | });
51 |
52 | it('using simple topology (srcMount = "/")', function () {
53 | return runTest({
54 | srcDir: 'dist',
55 | srcMount: '/',
56 | libMount: '/lib',
57 | nodeMount: '/$node'
58 | });
59 | });
60 |
61 | it('using complex mountpoints', function () {
62 | return runTest({
63 | srcDir: 'dist',
64 | srcMount: '/foo/bar/modules',
65 | libMount: '/baz/lib',
66 | nodeMount: '/$node'
67 | })
68 | });
69 |
70 | it('serving sources from the root', function () {
71 | return runTest({
72 | srcDir: '.',
73 | srcMount: '/foo/bar/modules',
74 | libMount: '/baz/lib',
75 | nodeMount: '/$node'
76 | })
77 | });
78 |
79 | afterEach(function () {
80 | staticServer.close();
81 | });
82 | });
83 | })
84 |
85 |
86 | });
--------------------------------------------------------------------------------
/test/e2e/project-fixtures.ts:
--------------------------------------------------------------------------------
1 | import {PackageBuilder, default as projectDriver} from "../../test-kit/project-driver";
2 | import tmp = require('tmp');
3 | import {SynchrounousResult} from "tmp";
4 | import {supportedLibs} from "../../src/node-support";
5 |
6 |
7 | export function setupProject(srcDir: string): PackageBuilder {
8 | const tempDir: SynchrounousResult = tmp.dirSync();
9 | const project = projectDriver(tempDir.name)
10 | .addMainFile(`${srcDir}/main.js`,`
11 | var a = require("./a");
12 | var x = require("pkgX");
13 | var x2 = require("pkgX/sub");
14 | `)
15 | .addFile(`${srcDir}/a.js`, supportedLibs.map(libName => `var ${libName} = require("${libName}");`).join('\n'));
16 |
17 | const pkgX = project.addPackage('pkgX')
18 | .addMainFile('x.js', `
19 | var y = require("pkgY");
20 | var data = require("./data.json");
21 | var bar = require("./foo/bar");
22 | var qux = require("./foo/bar/baz/qux");
23 | var sub = require("./bus");
24 | `)
25 | .addToPackageJson({
26 | browser: {
27 | './bus.js': './sub.js'
28 | }
29 | })
30 | .addFile('sub.js','')
31 | .addFile('data.json', '{ "wtf": "data" }')
32 | .addFile('foo/bar/index.js')
33 | .addFile('foo/bar/baz/qux.js', 'var bar = require("..");')
34 | .addPackage('pkgY').addMainFile('y.js', '');
35 |
36 | project.addFile('node_modules/brokenPkg/package.json', 'some_garbage_but_bundless_should_cope #$%@^');
37 |
38 |
39 | return project;
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/test/integration/api.spec.ts:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai';
2 | import {hookSystemJs} from '../../src/api';
3 | import {getProjectMap, ProjectMap} from "../../src/project-mapper";
4 | import {PackageBuilder, default as projectDriver} from "../../test-kit/project-driver";
5 | import {Topology} from "../../src/types";
6 | import {generateProjectInfo} from "../../src/defaults";
7 | import tmp = require('tmp');
8 | import * as Promise from 'bluebird';
9 |
10 | const SystemJS = (typeof System === 'undefined') ? require('systemjs/dist/system.src') : System;
11 | const SysConstructor = SystemJS.constructor;
12 |
13 | describe('system-hooks', function () {
14 | // https://github.com/systemjs/systemjs/issues/366#issuecomment-180057616
15 | let system, project, topology;
16 |
17 | beforeEach(() => {
18 | system = new SysConstructor();
19 | const tempDir = tmp.dirSync();
20 | project = projectDriver(tempDir.name);
21 | topology = {
22 | rootDir: project.getPath(),
23 | srcDir: 'dist',
24 | srcMount: '/local',
25 | libMount: '/__lib',
26 | nodeMount: '/$node',
27 | systemMount: '/$system',
28 | mapper: {
29 | nodeLibs: false
30 | }
31 | };
32 | system['fetch'] = function fetch(load) {
33 | expect(load.address).to.contain(topology.libMount);
34 | let path = load.address.substr(load.address.indexOf(topology.libMount)).replace(topology.libMount, 'node_modules');
35 | return Promise.resolve(project.readFile(path));
36 | };
37 | });
38 | it('normalize works with simple map', () => {
39 | project.addPackage('x')
40 | .addMainFile('index.js', `
41 | module.exports = require('./z');
42 | `)
43 | .addFile('z.js', `
44 | var yz = require('y/z');
45 | module.exports.foo = yz.bar;
46 | `);
47 | project.addPackage('y').addFile('z.js', `
48 | module.exports.bar = 'baz';
49 | `);
50 | hookSystemJs(system, '__base', getProjectMap(generateProjectInfo(topology)));
51 | return system.import('x').then((imported) => {
52 | expect(imported.foo).to.eql('baz');
53 | });
54 | });
55 | it('normalize respects noJSExtension', () => {
56 | project.addPackage('x')
57 | .addMainFile('index.js', `
58 | module.exports = require('./FOO');
59 | `)
60 | .addFile('FOO', `
61 | var yz = require('y/z');
62 | module.exports.foo = yz.bar;
63 | `);
64 | project.addPackage('y').addFile('z.js', `
65 | module.exports.bar = 'baz';
66 | `);
67 | hookSystemJs(system, '__base', getProjectMap(generateProjectInfo(topology)), undefined, undefined, /FOO/);
68 | return system.import('x').then((imported) => {
69 | expect(imported.foo).to.eql('baz');
70 | });
71 | });
72 | });
--------------------------------------------------------------------------------
/test/integration/normalize.spec.ts:
--------------------------------------------------------------------------------
1 | import {hookSystemJs} from "../../src/api";
2 | import {ProjectMap} from "../../src/project-mapper";
3 | import {expect} from "chai";
4 | const SystemJS = (typeof System === 'undefined') ? require('systemjs/dist/system.src') : System;
5 |
6 | const originalNormalizeFn = SystemJS['normalize'].bind(SystemJS);
7 |
8 | describe('System.normalize() hook', function () {
9 | let projectMap: ProjectMap;
10 |
11 | ['https://localhost:3000/', 'https://localhost:3000/complex/path/'].forEach(baseUrl => {
12 | describe(`with baseUrl = ${baseUrl}`, function () {
13 | before(function () {
14 | projectMap = {
15 | libMount: 'lib',
16 | packages: {
17 | pkgX: { p: '/lib/pkgX', m: 'x.js', r: { './funky.js': './monkey.js' } }
18 | },
19 | dirs: []
20 | };
21 | hookSystemJs(SystemJS, baseUrl, projectMap);
22 | });
23 |
24 | after(function () {
25 | SystemJS['normalize'] = originalNormalizeFn;
26 | });
27 |
28 | it('resolves correctly package url', function () {
29 | return SystemJS.normalize('pkgX', `${baseUrl}index.js`)
30 | .then(result => expect(result).to.eql(`${baseUrl}lib/pkgX/x.js`));
31 | });
32 |
33 | it('resolves correctly remapped url 1', function () {
34 | return SystemJS.normalize('./funky', `${baseUrl}lib/pkgX/x.js`)
35 | .then(result => expect(result).to.eql(`${baseUrl}lib/pkgX/monkey.js`));
36 | });
37 | });
38 | });
39 |
40 |
41 | });
42 |
--------------------------------------------------------------------------------
/test/integration/remap.spec.ts:
--------------------------------------------------------------------------------
1 | import projectDriver from '../../test-kit/project-driver';
2 | import tmp = require('tmp');
3 | import {SynchrounousResult} from "tmp";
4 | import {expect, use} from "chai";
5 | import {PackageBuilder} from "../../test-kit/project-driver";
6 | import {hookSystemJs} from '../../src/api';
7 | import {getProjectMap} from "../../src/project-mapper";
8 | import {generateProjectInfo, defTopology} from "../../src/defaults";
9 | import _ = require('lodash');
10 |
11 | const SystemJS = (typeof System === 'undefined') ? require('systemjs/dist/system.src') : System;
12 | const SysConstructor = SystemJS.constructor;
13 |
14 | use(require('chai-as-promised'));
15 |
16 | // These test cases taken from:
17 | // https://github.com/substack/node-browserify/tree/master/test/browser_field_resolve
18 |
19 | describe('file remapping', function () {
20 | const baseUrl = 'https://localhost:3000/';
21 | const libMount = 'lib';
22 | const entryPoint = `${baseUrl}${libMount}/a/index.js`;
23 |
24 | let project: PackageBuilder;
25 | let tempDir;
26 |
27 | const setup = () => {
28 | const system = new SysConstructor();
29 | const topology = _.assign({}, defTopology, { rootDir: tempDir.name });
30 | const projectMap = getProjectMap(generateProjectInfo(topology));
31 | hookSystemJs(system, baseUrl, projectMap);
32 | return (moduleId: string) => system.normalize(moduleId, entryPoint);
33 | };
34 |
35 | beforeEach(function () {
36 | tempDir = tmp.dirSync();
37 | project = projectDriver(tempDir.name).addPackage('a');
38 | });
39 |
40 | afterEach(function () {
41 | project.dispose();
42 | });
43 |
44 | it('a', function () {
45 | project
46 | .addToPackageJson({
47 | browser: {
48 | 'zzz': 'aaa'
49 | }
50 | })
51 | .addPackage('aaa')
52 | .addMainFile('main.js');
53 | const normalize = setup();
54 | return normalize('zzz').then(result => expect(result).to.equal(`${baseUrl}${libMount}/a/node_modules/aaa/main.js`));
55 | });
56 |
57 | it('b', function () {
58 | project
59 | .addToPackageJson({
60 | browser: {
61 | "zzz": "./x"
62 | }
63 | })
64 | .addFile('x.js')
65 | .addPackage('aaa')
66 | .addMainFile('main.js');
67 | const normalize = setup();
68 | return normalize('zzz').then(result => expect(result).to.equal(`${baseUrl}${libMount}/a/x.js`));
69 | });
70 |
71 | it('c', function () {
72 | project
73 | .addToPackageJson({
74 | browser: {
75 | "./z": "./x"
76 | }
77 | })
78 | .addFile('x.js')
79 | .addPackage('aaa')
80 | .addMainFile('main.js');
81 | const normalize = setup();
82 | return normalize('./z.js').then(result => expect(result).to.equal(`${baseUrl}${libMount}/a/x.js`));
83 | });
84 |
85 | it('d', function () {
86 | project
87 | .addToPackageJson({
88 | browser: {
89 | "./z.js": "./x.js"
90 | }
91 | })
92 | .addFile('x.js')
93 | .addPackage('aaa')
94 | .addMainFile('main.js');
95 | const normalize = setup();
96 | return normalize('./z.js').then(result => expect(result).to.equal(`${baseUrl}${libMount}/a/x.js`));
97 | });
98 |
99 | it('e', function () {
100 | project
101 | .addToPackageJson({
102 | browser: {
103 | "./z": "./x.js"
104 | }
105 | })
106 | .addFile('x.js')
107 | .addPackage('aaa')
108 | .addMainFile('main.js');
109 | const normalize = setup();
110 | return normalize('./z.js').then(result => expect(result).to.equal(`${baseUrl}${libMount}/a/x.js`));
111 | });
112 |
113 | it('f', function () {
114 | const baseUrl = 'https://localhost:3000/';
115 | project
116 | .addToPackageJson({
117 | browser: {
118 | "aaa/what": "./x.js"
119 | }
120 | })
121 | .addFile('x.js')
122 | .addPackage('aaa')
123 | .addMainFile('main.js');
124 | const normalize = setup();
125 | return normalize('aaa/what.js').then(result => expect(result).to.equal(`${baseUrl}${libMount}/a/x.js`));
126 | });
127 |
128 | it('g', function () {
129 | project
130 | .addToPackageJson({
131 | browser: {
132 | "./x.js": false
133 | }
134 | })
135 | .addFile('x.js')
136 | .addPackage('aaa')
137 | .addMainFile('main.js');
138 | const normalize = setup();
139 | return expect(normalize('./x')).to.be.rejected;
140 | });
141 |
142 | it('h', function () {
143 | project
144 | .addToPackageJson({
145 | browser: {
146 | "./x.js": false
147 | }
148 | })
149 | .addFile('x.js')
150 | .addPackage('aaa')
151 | .addMainFile('main.js');
152 | const normalize = setup();
153 | return expect(normalize('./x.js')).to.be.rejected;
154 | });
155 |
156 | it('i', function () {
157 | project
158 | .addToPackageJson({
159 | browser: {
160 | "./x": "./browser"
161 | }
162 | })
163 | .addFile('x.js')
164 | .addPackage('aaa')
165 | .addMainFile('main.js');
166 | const normalize = setup();
167 | return normalize('./x.js').then(result => expect(result).to.equal(`${baseUrl}${libMount}/a/browser.js`));
168 | });
169 |
170 | it('j', function () {
171 | project
172 | .addToPackageJson({
173 | browser: {
174 | "./x.js": "./browser.js"
175 | }
176 | })
177 | .addFile('x.js')
178 | .addPackage('aaa')
179 | .addMainFile('main.js');
180 | const normalize = setup();
181 | return normalize('./x').then(result => expect(result).to.equal(`${baseUrl}${libMount}/a/browser.js`));
182 | });
183 |
184 | it('k', function () {
185 | project
186 | .addPackage('x')
187 | .addFile('hey.js')
188 | .addToPackageJson({
189 | browser: {
190 | "./zzz": "./hey"
191 | }
192 | });
193 |
194 | const normalize = setup();
195 | return normalize('x/zzz').then(result => expect(result).to.equal(`${baseUrl}${libMount}/a/node_modules/x/hey.js`));
196 | });
197 |
198 | it('l', function () {
199 | project
200 | .addPackage('x')
201 | .addFile('hey.js')
202 | .addToPackageJson({
203 | browser: {
204 | "./zzz.js": "./hey"
205 | }
206 | });
207 |
208 | const normalize = setup();
209 | return normalize('x/zzz').then(result => expect(result).to.equal(`${baseUrl}${libMount}/a/node_modules/x/hey.js`));
210 | });
211 |
212 | // Our own additions to this test suite
213 |
214 | it('extra a', function () {
215 | project
216 | .addPackage('aaa')
217 | .addMainFile('main.js')
218 | .addToPackageJson({
219 | browser: {
220 | './main.js': './x.js'
221 | }
222 | });
223 | const normalize = setup();
224 | return normalize('aaa').then(result => expect(result).to.equal(`${baseUrl}${libMount}/a/node_modules/aaa/x.js`));
225 | });
226 |
227 | it('extra b', function () {
228 | project
229 | .addPackage('aaa')
230 | .addToPackageJson({
231 | browser: {
232 | './index.js': './x.js'
233 | }
234 | });
235 | const normalize = setup();
236 | return normalize('aaa').then(result => expect(result).to.equal(`${baseUrl}${libMount}/a/node_modules/aaa/x.js`));
237 | });
238 | });
239 |
--------------------------------------------------------------------------------
/test/unit/locator.spec.ts:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai';
2 | import {ProjectMap} from "../../src/project-mapper";
3 | import * as locator from '../../src/client/locator';
4 | import {parseUrl, ParsedUrl, applyFileRemapping, joinUrl} from "../../src/client/locator";
5 |
6 | describe('locate', function () {
7 | let projectMap: ProjectMap;
8 | let preProcess: { (name: string, parentName?: string, parentAddress?: string): string };
9 | let postProcess: { (resolvedName: string): string };
10 |
11 | const baseUrl = 'https://localhost:3000/';
12 | const libMount = 'lib';
13 |
14 | before(function () {
15 | projectMap = {
16 | libMount,
17 | packages: {},
18 | dirs: []
19 | };
20 | preProcess = locator.preProcess.bind(null, projectMap, baseUrl);
21 | postProcess = locator.postProcess.bind(null, projectMap, baseUrl);
22 | });
23 |
24 | describe('preProcess()', function () {
25 |
26 | before(function () {
27 | projectMap.packages = {
28 | pkgX: { p: '/lib/pkgX', m: 'x.js' },
29 | pkgY: { p: '/lib/pkgX/node_modules/pkgY', m: 'y.js'},
30 | zlib: { p: '/$node/browserify-zlib', m: 'src/index.js' }
31 | };
32 | });
33 |
34 | it('appends automagically .js extension', function () {
35 | expect(preProcess('./a')).to.equal('./a.js');
36 | expect(preProcess('pkgX/data.json')).to.equal(`${baseUrl}lib/pkgX/data.json`);
37 | expect(preProcess('src/client/editor/editor.skin.html')).to.equal('src/client/editor/editor.skin.html.js');
38 | });
39 |
40 | it('reduces superfluous slashes', function () {
41 | expect(preProcess('..//a')).to.equal('../a.js');
42 | expect(preProcess('http://host:8080/foo//bar')).to.equal('http://host:8080/foo/bar.js');
43 | });
44 |
45 | it('finds package main file', function () {
46 | expect(preProcess('pkgX')).to.equal(`${baseUrl}lib/pkgX/x.js`);
47 | });
48 |
49 | it('finds sub module in a package', function () {
50 | expect(preProcess('pkgX/sub')).to.equal(`${baseUrl}lib/pkgX/sub.js`)
51 | });
52 |
53 | it('deals with relative paths', function () {
54 | expect(preProcess('./elliptic')).to.equal('./elliptic.js');
55 | expect(preProcess('../elliptic')).to.equal('../elliptic.js');
56 | expect(preProcess('../../elliptic')).to.equal('../../elliptic.js');
57 | expect(preProcess('..')).to.equal('..');
58 | expect(preProcess('../.')).to.equal('../.');
59 | });
60 |
61 |
62 | it('finds node package', function () {
63 | expect(preProcess('zlib')).to.equal(`${baseUrl}$node/browserify-zlib/src/index.js`);
64 | });
65 | });
66 |
67 | describe('postProcess()', function () {
68 |
69 | before(function () {
70 | projectMap.packages = {
71 | pkgX: { p: '/lib/pkgX', m: 'x.js', r: { './funky.js': './monkey.js' } },
72 | pkgY: { p: '/lib/pkgX/node_modules/pkgY', m: 'y.js'},
73 | };
74 | projectMap.dirs = [
75 | '/a/b.js',
76 | '/lib/pkgX/foo/bar.js'
77 | ];
78 | });
79 |
80 | it('applies file remapping', function () {
81 | expect(postProcess(`${baseUrl}lib/pkgX/funky.js`)).to.equal(`${baseUrl}lib/pkgX/monkey.js`)
82 | });
83 |
84 | it('identifies default index file in a directory', function () {
85 | expect(postProcess(`${baseUrl}a/b.js`)).to.equal(`${baseUrl}a/b/index.js`);
86 | expect(postProcess(`${baseUrl}a/b/`)).to.equal(`${baseUrl}a/b/index.js`);
87 | expect(postProcess(`${baseUrl}a/c.js`)).to.equal(`${baseUrl}a/c.js`);
88 | expect(postProcess(`${baseUrl}lib/pkgX/foo/bar.js`)).to.equal(`${baseUrl}lib/pkgX/foo/bar/index.js`);
89 | expect(postProcess(`${baseUrl}lib/pkgX/foo/bar/`)).to.equal(`${baseUrl}lib/pkgX/foo/bar/index.js`);
90 | });
91 |
92 | it('resolves result of System.normalize() as package', function () {
93 | expect(postProcess(`${baseUrl}lib/pkgX/node_modules/pkgY`)).to.equal(`${baseUrl}lib/pkgX/node_modules/pkgY/y.js`);
94 | expect(postProcess(`${baseUrl}lib/pkgX/node_modules/pkgY/`)).to.equal(`${baseUrl}lib/pkgX/node_modules/pkgY/y.js`);
95 | });
96 | });
97 |
98 | describe('joinUrl()', function () {
99 | it('correctly joins base Url and paths', function () {
100 | const expectedResult = 'https://localhost:3000/a/b.js';
101 | expect(joinUrl('https://localhost:3000', 'a', 'b.js')).to.equal(expectedResult);
102 | expect(joinUrl('https://localhost:3000/', '/a', 'b.js')).to.equal(expectedResult);
103 | expect(joinUrl('https://localhost:3000', '/a/', '/b.js')).to.equal(expectedResult);
104 | expect(joinUrl('https://localhost:3000', 'a', './b.js')).to.equal(expectedResult);
105 | });
106 |
107 | });
108 |
109 | describe('parseUrl()', function () {
110 | it('parses url (1)', function () {
111 | expect(parseUrl(`${baseUrl}${libMount}/pkgX/foo/bar/x.js`, baseUrl, libMount)).to.eql({
112 | pkg: 'pkgX',
113 | pkgPath: 'lib/pkgX',
114 | localPath: 'foo/bar/x.js',
115 | ext: '.js'
116 | });
117 | });
118 | it('parses url (2)', function () {
119 | expect(parseUrl(`${baseUrl}${libMount}/pkgX`, baseUrl, libMount)).to.eql({
120 | pkg: 'pkgX',
121 | pkgPath: 'lib/pkgX',
122 | localPath: '',
123 | ext: ''
124 | });
125 | });
126 | it('parses url (3)', function () {
127 | expect(parseUrl(`${baseUrl}${libMount}/pkgX/`, baseUrl, libMount)).to.eql({
128 | pkg: 'pkgX',
129 | pkgPath: 'lib/pkgX',
130 | localPath: '',
131 | ext: ''
132 | });
133 | });
134 | it('parses url (4)', function () {
135 | expect(parseUrl(`${baseUrl}${libMount}/pkgX/node_modules/pkgY/y.js`, baseUrl, libMount)).to.eql({
136 | pkg: 'pkgY',
137 | pkgPath: 'lib/pkgX/node_modules/pkgY',
138 | localPath: 'y.js',
139 | ext: '.js'
140 | });
141 | });
142 | it('Ignores non-lib path', function () {
143 | expect(parseUrl(`${baseUrl}a/b/c/d.js`, baseUrl, libMount)).to.eql({
144 | pkg: '',
145 | pkgPath: '',
146 | localPath: 'a/b/c/d.js',
147 | ext: '.js'
148 | });
149 | });
150 |
151 | it('Deals with complex libMount', function () {
152 | expect(parseUrl(`${baseUrl}a/b/c/d.js`, baseUrl, 'a/b')).to.eql({
153 | pkg: 'c',
154 | pkgPath: 'a/b/c',
155 | localPath: 'd.js',
156 | ext: '.js'
157 | });
158 | });
159 | });
160 |
161 | describe('applyFileRemapping()', function () {
162 | it('applies file remapping', function () {
163 | const projectMap: ProjectMap = {
164 | libMount,
165 | packages: {
166 | pkgX: { p: '/lib/pkgX', m: 'x.js', r: { './funky.js': './monkey.js' } }
167 | },
168 | dirs: []
169 | };
170 | const url: ParsedUrl = {
171 | pkg: 'pkgX',
172 | pkgPath: 'pkgX',
173 | localPath: 'funky.js',
174 | ext: '.js'
175 | };
176 | expect(applyFileRemapping(projectMap, url)).to.eql('pkgX/monkey.js');
177 | });
178 |
179 | it('detects correctly the main file (1)', function () {
180 | const projectMap: ProjectMap = {
181 | libMount,
182 | packages: {
183 | pkgX: { p: '/lib/pkgX', m: 'x.js', r: { './funky.js': './monkey.js' } }
184 | },
185 | dirs: []
186 | };
187 | const url: ParsedUrl = {
188 | pkg: 'pkgX',
189 | pkgPath: 'pkgX',
190 | localPath: '',
191 | ext: '.js'
192 | };
193 | expect(applyFileRemapping(projectMap, url)).to.eql('pkgX/x.js');
194 | });
195 |
196 | it('detects correctly the main file (dot in front of the main file path)', function () {
197 | const projectMap: ProjectMap = {
198 | libMount,
199 | packages: {
200 | pkgX: { p: '/lib/pkgX', m: './x.js', r: { './funky.js': './monkey.js' } }
201 | },
202 | dirs: []
203 | };
204 | const url: ParsedUrl = {
205 | pkg: 'pkgX',
206 | pkgPath: 'pkgX',
207 | localPath: '',
208 | ext: '.js'
209 | };
210 | expect(applyFileRemapping(projectMap, url)).to.eql('pkgX/x.js');
211 | });
212 |
213 | it('detects correctly the main file (main file path a result of remapping)', function () {
214 | const projectMap: ProjectMap = {
215 | libMount,
216 | packages: {
217 | pkgX: { p: '/lib/pkgX', m: './main.js', r: { './main.js': './x.js' } }
218 | },
219 | dirs: []
220 | };
221 | const url: ParsedUrl = {
222 | pkg: 'pkgX',
223 | pkgPath: 'pkgX',
224 | localPath: '',
225 | ext: '.js'
226 | };
227 | expect(applyFileRemapping(projectMap, url)).to.eql('pkgX/x.js');
228 | });
229 |
230 | });
231 |
232 | });
233 |
234 |
--------------------------------------------------------------------------------
/test/unit/project-mapper.spec.ts:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai';
2 | import {PackageBuilder} from "../../test-kit/project-driver";
3 | import projectDriver from "../../test-kit/project-driver";
4 | import tmp = require('tmp');
5 | import {getProjectMap, ProjectMap} from "../../src/project-mapper";
6 | import {BootstrapScriptOptions} from "../../src/types";
7 | import {generateProjectInfo} from "../../src/defaults";
8 |
9 | describe('Project Mapper', function () {
10 | let tempDir;
11 | let project: PackageBuilder;
12 | let projectMap: ProjectMap;
13 | let topology: BootstrapScriptOptions;
14 |
15 | beforeEach(function () {
16 | tempDir = tmp.dirSync();
17 | project = projectDriver(tempDir.name);
18 | topology = {
19 | rootDir: project.getPath(),
20 | srcDir: 'dist',
21 | srcMount: '/local',
22 | libMount: '/lib',
23 | nodeMount: '/$node',
24 | mapper: {
25 | nodeLibs: false
26 | }
27 | }
28 | });
29 |
30 | describe('describes packages in project', function () {
31 | beforeEach(function () {
32 | project
33 | .addMainFile('dist/main.js')
34 | .addPackage('foo').addMainFile('bar/far/f.js');
35 | project
36 | .addPackage('la')
37 | .addMainFile('index.js')
38 | .addBrowserMainFile('browser.js');
39 | project
40 | .addPackage('sol').addJspmMainFile('la/si/do.js');
41 |
42 | project
43 | .addPackage('do')
44 | .addToPackageJson({
45 | browser: {
46 | './bus.js': './sub.js'
47 | }
48 | });
49 |
50 | projectMap = getProjectMap(generateProjectInfo(topology));
51 | });
52 |
53 | it('as correct project map', function () {
54 | expect(projectMap.libMount).to.eql('lib');
55 | expect(projectMap.packages).to.eql({
56 | 'foo': { p: '/lib/foo', m: 'bar/far/f.js' },
57 | 'la': { p: '/lib/la', m: 'browser.js' },
58 | 'sol': { p: '/lib/sol', m: 'la/si/do.js' },
59 | 'do': { p: '/lib/do', m: 'index.js', r: { './bus.js': './sub.js' } }
60 | });
61 | });
62 | });
63 |
64 | describe('resolves different versions', function () {
65 | beforeEach(function () {
66 | project
67 | .addMainFile('dist/main.js')
68 | .addPackage('foo')
69 | .addPackage('webpack', '1.2.3')
70 | .addPackage('socket.io', '7.8.9');
71 |
72 |
73 | project
74 | .addPackage('bar')
75 | .addPackage('webpack', '2.3.4')
76 | .addPackage('socket.io', '4.5.6');
77 |
78 | projectMap = getProjectMap(generateProjectInfo(topology));
79 | });
80 |
81 | it('with "aggressive" approach', function () {
82 | expect(projectMap.packages).to.eql({
83 | 'foo': { p: '/lib/foo', m: 'index.js' },
84 | 'bar': { p: '/lib/bar', m: 'index.js' },
85 | 'webpack': { p: '/lib/bar/node_modules/webpack', m: 'index.js' },
86 | 'socket.io': { p: '/lib/foo/node_modules/webpack/node_modules/socket.io', m: 'index.js'}
87 | });
88 | });
89 | });
90 |
91 | describe('among multiple instances of the same version', function () {
92 | beforeEach(function () {
93 | project
94 | .addPackage('webpack', '1.2.3');
95 |
96 | project
97 | .addPackage('foo')
98 | .addPackage('webpack', '1.2.3');
99 |
100 | projectMap = getProjectMap(generateProjectInfo(topology));
101 | });
102 |
103 | it('preferes package closer to the top level', function () {
104 | expect(projectMap.packages).to.eql({
105 | 'foo': { p: '/lib/foo', m: 'index.js' },
106 | 'webpack': { p: '/lib/webpack', m: 'index.js' }
107 | });
108 | });
109 | });
110 |
111 | describe('describes Node.js packages', function () {
112 | const npm2nodeLibsDir = '/$node/node-libs-browser/node_modules';
113 | const npm3nodeLibsDir = '/$node';
114 | beforeEach(function () {
115 | project
116 | .addMainFile('dist/main.js');
117 | topology.mapper.nodeLibs = true;
118 | projectMap = getProjectMap(generateProjectInfo(topology));
119 | });
120 |
121 | it('finds regular (ported) Node module', function () {
122 | expect(projectMap.packages['path'].p).to.be.oneOf([
123 | `${npm2nodeLibsDir}/path-browserify`,
124 | `${npm3nodeLibsDir}/path-browserify`
125 | ]);
126 | expect(projectMap.packages['path'].m).to.equal('index.js');
127 | });
128 |
129 | it('finds stubbed Node module', function () {
130 | expect(projectMap.packages['child_process']).to.eql({ p: '/$node', m: 'node-support/stub.js' });
131 | });
132 |
133 | it('finds Node module with explicit browser version', function () {
134 | expect(projectMap.packages['os'].p).to.be.oneOf([
135 | `${npm2nodeLibsDir}/os-browserify`,
136 | `${npm3nodeLibsDir}/os-browserify`,
137 | ]);
138 | expect(projectMap.packages['os'].m).to.equal('browser.js');
139 | });
140 | });
141 |
142 | describe('describes default index files', function () {
143 | beforeEach(function () {
144 | project
145 | .addFile('dist/foo/bar/index.js')
146 | .addPackage('x').addFile('z/index.js')
147 | .addPackage('poo').addFile('index.js', '// this should be invisible');
148 | project
149 | .addPackage('y')
150 | .addFile('a.index')
151 | .addPackage('z')
152 | .addFile('a/b/c/index.js');
153 | projectMap = getProjectMap(generateProjectInfo(topology));
154 | });
155 |
156 | it('in various depths', function () {
157 | expect(projectMap.dirs).to.eql([
158 | '/local/foo/bar.js',
159 | '/lib/x/z.js',
160 | '/lib/y/node_modules/z/a/b/c.js'
161 | ]);
162 | });
163 | });
164 |
165 | describe('in topology where srcDir contains the whole project', function () {
166 | beforeEach(function () {
167 | topology.srcDir = '';
168 |
169 | project
170 | .addFile('dist/foo/bar/index.js')
171 | .addPackage('x').addFile('z/index.js')
172 | .addPackage('poo').addFile('index.js', '// this should be invisible');
173 | project
174 | .addPackage('y')
175 | .addFile('a.index')
176 | .addPackage('z')
177 | .addFile('a/b/c/index.js');
178 | projectMap = getProjectMap(generateProjectInfo(topology));
179 | });
180 |
181 | it('index files are collected correctly', function () {
182 | expect(projectMap.dirs).to.eql([
183 | '/local/dist/foo/bar.js',
184 | '/lib/x/z.js',
185 | '/lib/y/node_modules/z/a/b/c.js'
186 | ]);
187 | });
188 | });
189 |
190 |
191 | });
192 |
--------------------------------------------------------------------------------
/test/unit/url-resolver.spec.ts:
--------------------------------------------------------------------------------
1 | import {resolveUrlToFile} from "../../src/url-resolver";
2 | import {expect} from 'chai';
3 | import {Topology} from "../../src/types";
4 | import path = require('path');
5 |
6 | describe('url resolver', function () {
7 | describe('with default topology', function () {
8 | let topology: Topology;
9 | let resolve;
10 | before(function () {
11 | topology = {
12 | rootDir: '/root',
13 | srcDir: 'dist',
14 | srcMount: '/',
15 | libMount: '/node_modules',
16 | nodeMount: '/$node'
17 | };
18 | resolve = resolveUrlToFile.bind(null, topology);
19 | });
20 |
21 | it('resolves source file', function () {
22 | expect(resolve('/a.js')).to.equal(path.normalize('/root/dist/a.js'));
23 | });
24 |
25 | it('resolves package file', function () {
26 | expect(resolve('/node_modules/pkgX/foo/bar/a.js')).to.equal(path.normalize('/root/node_modules/pkgX/foo/bar/a.js'));
27 | });
28 | });
29 |
30 | describe('with custom topology', function () {
31 | let topology: Topology;
32 | let resolve;
33 | before(function () {
34 | topology = {
35 | rootDir: '/root',
36 | srcDir: 'dist',
37 | srcMount: '/local',
38 | libMount: '/lib',
39 | nodeMount: '/$node'
40 | };
41 | resolve = resolveUrlToFile.bind(null, topology);
42 | });
43 |
44 | it('resolves source file', function () {
45 | expect(resolve('/local/a.js')).to.equal(path.normalize('/root/dist/a.js'));
46 | });
47 |
48 | it('resolves package file', function () {
49 | expect(resolve('/lib/pkgX/foo/bar/a.js')).to.equal(path.normalize('/root/node_modules/pkgX/foo/bar/a.js'));
50 | });
51 | });
52 |
53 | });
54 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2015",
4 | "module": "commonjs",
5 | "declaration": true,
6 | "sourceMap": true,
7 | "outDir": "dist"
8 | },
9 | "exclude": [
10 | "node_modules",
11 | "dist"
12 | ]
13 | }
--------------------------------------------------------------------------------