├── .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 | } --------------------------------------------------------------------------------