├── .clang-format ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── koa-module-specifier-transform.ts ├── koa-node-resolve.ts ├── support │ ├── logger.ts │ ├── parse5-utils.ts │ ├── path-utils.ts │ ├── resolve-node-specifier.ts │ └── string-utils.ts ├── test │ ├── koa-module-specifier-transform.test.ts │ ├── koa-node-resolve.test.ts │ ├── logger.test.ts │ ├── path-utils.test.ts │ ├── resolve-node-specifier.test.ts │ ├── test-utils.test.ts │ └── test-utils.ts ├── transform-html.ts └── transform-js-module.ts ├── test └── fixtures │ ├── node_modules │ ├── no-imports-or-exports │ │ ├── main.js │ │ └── package.json │ ├── x │ │ ├── main.js │ │ └── package.json │ ├── y │ │ ├── jsnext.js │ │ ├── main.js │ │ └── package.json │ └── z │ │ ├── binary-file.node │ │ ├── jsnext.js │ │ ├── main.js │ │ ├── module.js │ │ └── package.json │ └── some-file.js ├── tsconfig.json └── tslint.json /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | AlignAfterOpenBracket: AlwaysBreak 3 | AllowAllParametersOfDeclarationOnNextLine: false 4 | AllowShortBlocksOnASingleLine: false 5 | AllowShortCaseLabelsOnASingleLine: false 6 | AllowShortFunctionsOnASingleLine: None 7 | AllowShortIfStatementsOnASingleLine: false 8 | AllowShortLoopsOnASingleLine: false 9 | BinPackArguments: false 10 | # This breaks async functions sometimes, see 11 | # https://github.com/Polymer/polymer-analyzer/pull/393 12 | # BinPackParameters: false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/* 2 | /lib/* 3 | /*.tgz 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | dist: xenial 5 | cache: 6 | directories: 7 | - node_modules 8 | script: 9 | - npm run build 10 | - npm test 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | 12 | 13 | 14 | 15 | 16 | ## [1.0.0-pre.9] - 2020-07-29 17 | - Updated `@types/babel__generator` so we could remove the local declarations for missing GeneratorOptions. 18 | 19 | ## [1.0.0-pre.8] - 2020-07-06 20 | - Fixed an issue with module resolution of specifiers inside of HTML files being served from server root route. 21 | 22 | ## [1.0.0-pre.7] - 2020-04-09 23 | - Added support for Windows paths (thanks, @andrewiggins) 24 | 25 | ## [1.0.0-pre.6] - 2019-08-06 26 | - Fixed issue where syntax errors encountered in files would result in an empty response. 27 | 28 | ## [1.0.0-pre.5] - 2019-06-25 29 | - Fixed issue where `nodeResolve()` did not properly effect the level of the provided `logger` where individual specifier transform logging was concerned. 30 | - Fix `Module '@babel/generator' resolves to an untyped module` error for TypeScript users. 31 | - Fix issue where html/head/body tags were stripped out by default parser/serializer. 32 | 33 | ## [1.0.0-pre.4] - 2019-06-07 34 | - Added `logLevel` option which defaults to `warn` so info/debug transformations are supressed by default. 35 | - Added `dynamicImport`, `importMeta`, `exportDefaultFrom` and `exportNamespaceFrom` to default Babel parser configuration. 36 | - Added single-quoted strings, `retainFunctionParens` and `retainLines` to default Babel generator configuration. 37 | 38 | ## [1.0.0-pre.3] - 2019-06-06 39 | - Fix missing `@types/parse5` dependency for TypeScript users. 40 | - Fix invalid TypeScript typings related to the logger option. 41 | 42 | ## [1.0.0-pre.2] - 2019-06-05 43 | - Rewrites resolvable Node package specifiers in JavaScript module files. 44 | - Rewrites resolvable Node package specifiers in HTML files in ` 38 | 39 | 40 | Cool 41 | 42 | 43 | `, 44 | }, 45 | }, 46 | async (server) => { 47 | t.equal( 48 | squeeze((await request(server).get('/my-page.html')).text), 49 | squeeze(` 50 | 51 | 52 | 53 | 56 | 57 | 58 | Cool 59 | 60 | 61 | `), 62 | `should preserve existing html/head/body elements`); 63 | }); 64 | }); 65 | 66 | test('moduleSpecifierTransform callback returns undefined to noop', async (t) => { 67 | t.plan(3); 68 | const logger = testLogger(); 69 | createAndServe( 70 | { 71 | middleware: [moduleSpecifierTransform( 72 | (_baseURL, specifier, _logger) => 73 | specifier === 'y' ? './node_modules/y/index.js' : undefined, 74 | {logger, logLevel: 'info'})], 75 | routes: { 76 | '/my-module.js': ` 77 | import * as x from 'x'; 78 | import * as y from 'y'; 79 | `, 80 | '/my-page.html': ` 81 | 85 | `, 86 | }, 87 | }, 88 | async (server) => { 89 | t.equal( 90 | squeeze((await request(server).get('/my-module.js')).text), 91 | squeeze(` 92 | import * as x from 'x'; 93 | import * as y from './node_modules/y/index.js'; 94 | `), 95 | 'should transform only defined specifiers in external module'); 96 | t.equal( 97 | squeeze((await request(server).get('/my-page.html')).text), 98 | squeeze(` 99 | 103 | `), 104 | 'should transform only defined specifiers in inline module script'); 105 | t.deepEqual(logger.infos.map((args) => args.join(' ')), [ 106 | 'Transformed 1 module specifier(s) in "/my-module.js"', 107 | 'Transformed 1 module specifier(s) in "/my-page.html"', 108 | ]); 109 | }); 110 | }); 111 | 112 | test('moduleSpecifierTransform will convert dynamic imports', async (t) => { 113 | t.plan(3); 114 | const logger = testLogger(); 115 | createAndServe( 116 | { 117 | middleware: [moduleSpecifierTransform( 118 | (_baseURL, specifier, _logger) => 119 | `./node_modules/${specifier}/index.js`, 120 | {logger, logLevel: 'info'})], 121 | routes: { 122 | '/my-module.js': ` 123 | import('x').then((x) => x.doStuff()); 124 | `, 125 | '/my-page.html': ` 126 | 129 | `, 130 | } 131 | }, 132 | async (server) => { 133 | t.equal( 134 | squeeze((await request(server).get('/my-module.js')).text), 135 | // NOTE(usergenic): @babel/generator doesn't have an option to 136 | // retain parenthesis around single parameter anonymous functions, 137 | // so `(x) => ...` is unavoidably transformed to `x => ...` 138 | squeeze(` 139 | import('./node_modules/x/index.js').then(x => x.doStuff()); 140 | `), 141 | 'should transform dynamic import in external module'); 142 | t.equal( 143 | squeeze((await request(server).get('/my-page.html')).text), 144 | squeeze(` 145 | 148 | `), 149 | 'should transform dynamic import in inline module script'); 150 | t.deepEqual(logger.infos.map((args) => args.join(' ')), [ 151 | 'Transformed 1 module specifier(s) in "/my-module.js"', 152 | 'Transformed 1 module specifier(s) in "/my-page.html"', 153 | ]); 154 | }); 155 | }); 156 | 157 | test('moduleSpecifierTransform default parser configuration', async (t) => { 158 | t.plan(1); 159 | const logger = testLogger(); 160 | createAndServe( 161 | { 162 | middleware: [moduleSpecifierTransform( 163 | (_baseURL, specifier, _logger) => 164 | `./node_modules/${specifier}/index.js`, 165 | {logger})], 166 | routes: { 167 | '/my-module.js': ` 168 | export default from 'x'; 169 | export * as y from 'y'; 170 | async function* infiniteMeta() { 171 | while (true) { 172 | yield { ...import.meta }; 173 | } 174 | } 175 | ` 176 | } 177 | }, 178 | async (server) => { 179 | t.equal( 180 | squeeze((await request(server).get('/my-module.js')).text), 181 | squeeze(` 182 | export default from './node_modules/x/index.js'; 183 | export * as y from './node_modules/y/index.js'; 184 | async function* infiniteMeta() { 185 | while (true) { 186 | yield { ...import.meta }; 187 | } 188 | } 189 | `), 190 | 'should tolerate modern JavaScript syntax features'); 191 | }); 192 | }); 193 | 194 | test('moduleSpecifierTransform middleware logs errors', async (t) => { 195 | t.plan(3); 196 | const logger = testLogger(); 197 | createAndServe( 198 | { 199 | middleware: [moduleSpecifierTransform( 200 | (_baseURL, _specifier, _logger) => undefined, {logger})], 201 | routes: { 202 | '/my-module.js': ` 203 | this is a syntax error; 204 | `, 205 | '/my-page.html': ` 206 | 209 | `, 210 | }, 211 | }, 212 | async (server) => { 213 | t.equal( 214 | squeeze((await request(server).get('/my-module.js')).text), 215 | squeeze(` 216 | this is a syntax error; 217 | `), 218 | 'should leave a file with unparseable syntax error alone'); 219 | t.equal( 220 | squeeze((await request(server).get('/my-page.html')).text), 221 | squeeze(` 222 | 225 | `), 226 | 'should leave a file with unparseable inline module script alone'); 227 | t.deepEqual( 228 | logger.errors.map((args: unknown[]) => args.join(' ')), 229 | [ 230 | 'Unable to transform module specifiers in "/my-module.js" due to SyntaxError: Unexpected token, expected ";" (2:17)', 231 | 'Unable to transform module specifiers in "/my-page.html" due to SyntaxError: Unexpected token, expected ";" (2:19)', 232 | ], 233 | 'should log every error thrown'); 234 | }); 235 | }); 236 | 237 | test('moduleSpecifierTransform middleware logs callback error', async (t) => { 238 | t.plan(3); 239 | const logger = testLogger(); 240 | createAndServe( 241 | { 242 | middleware: [moduleSpecifierTransform( 243 | (_baseURL, _specifier, _logger) => { 244 | throw new Error('whoopsie daisy'); 245 | }, 246 | {logger})], 247 | routes: { 248 | '/my-module.js': ` 249 | import * as wubbleFlurp from 'wubble-flurp'; 250 | `, 251 | '/my-page.html': ` 252 | 255 | `, 256 | }, 257 | }, 258 | async (server) => { 259 | t.equal( 260 | squeeze((await request(server).get('/my-module.js')).text), 261 | squeeze(` 262 | import * as wubbleFlurp from 'wubble-flurp'; 263 | `), 264 | 'should not transform the external script when error occurs'); 265 | t.equal( 266 | squeeze((await request(server).get('/my-page.html')).text), 267 | squeeze(` 268 | 271 | `), 272 | 'should not transform inline script when error occurs'); 273 | t.deepEqual( 274 | logger.errors.map((args) => args.join(' ')), 275 | [ 276 | 'Unable to transform module specifiers in "/my-module.js" due to Error: whoopsie daisy', 277 | 'Unable to transform module specifiers in "/my-page.html" due to Error: whoopsie daisy', 278 | ], 279 | 'should log every error thrown'); 280 | }); 281 | }); 282 | -------------------------------------------------------------------------------- /src/test/koa-node-resolve.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2019 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | import request from 'supertest'; 15 | import test from 'tape'; 16 | 17 | import {nodeResolve} from '../koa-node-resolve'; 18 | import {resolvePathPreserveTrailingSlash} from '../support/path-utils'; 19 | 20 | import {createAndServe, squeeze, testLogger} from './test-utils'; 21 | 22 | const fixturesPath = 23 | resolvePathPreserveTrailingSlash(__dirname, '../../test/fixtures/'); 24 | 25 | test('nodeResolve middleware transforms resolvable specifiers', async (t) => { 26 | t.plan(4); 27 | const logger = testLogger(); 28 | createAndServe( 29 | { 30 | middleware: 31 | [nodeResolve({root: fixturesPath, logger, logLevel: 'debug'})], 32 | routes: { 33 | '/my-module.js': `import * as x from 'x';`, 34 | '/my-page.html': ` 35 | 38 | `, 39 | }, 40 | }, 41 | async (server) => { 42 | t.equal( 43 | squeeze((await request(server).get('/my-module.js')).text), 44 | squeeze(` 45 | import * as x from './node_modules/x/main.js'; 46 | `), 47 | 'should transform specifiers in JavaScript module'); 48 | t.equal( 49 | squeeze((await request(server).get('/my-page.html')).text), 50 | squeeze(` 51 | 54 | `), 55 | 'should transform specifiers in inline module script'); 56 | t.deepEqual(logger.debugs.map((args) => args.join(' ')), [ 57 | '[koa-node-resolve] Resolved Node module specifier "x" to "./node_modules/x/main.js"', 58 | '[koa-node-resolve] Resolved Node module specifier "x" to "./node_modules/x/main.js"', 59 | ]); 60 | t.deepEqual(logger.infos.map((args) => args.join(' ')), [ 61 | '[koa-node-resolve] Transformed 1 module specifier(s) in "/my-module.js"', 62 | '[koa-node-resolve] Transformed 1 module specifier(s) in "/my-page.html"', 63 | ]); 64 | }); 65 | }); 66 | 67 | test('nodeResolve middleware works even if baseURL has no pathname', async (t) => { 68 | t.plan(4); 69 | const logger = testLogger(); 70 | createAndServe( 71 | { 72 | middleware: 73 | [nodeResolve({root: fixturesPath, logger, logLevel: 'debug'})], 74 | routes: { 75 | '/my-module.js': `import * as x from 'x';`, 76 | '/': ` 77 | 80 | `, 81 | }, 82 | }, 83 | async (server) => { 84 | t.equal( 85 | squeeze((await request(server).get('/my-module.js')).text), 86 | squeeze(` 87 | import * as x from './node_modules/x/main.js'; 88 | `), 89 | 'should transform specifiers in JavaScript module'); 90 | t.equal( 91 | squeeze((await request(server).get('/')).text), 92 | squeeze(` 93 | 96 | `), 97 | 'should transform specifiers in inline module script'); 98 | t.deepEqual(logger.debugs.map((args) => args.join(' ')), [ 99 | '[koa-node-resolve] Resolved Node module specifier "x" to "./node_modules/x/main.js"', 100 | '[koa-node-resolve] Resolved Node module specifier "x" to "./node_modules/x/main.js"', 101 | ]); 102 | t.deepEqual(logger.infos.map((args) => args.join(' ')), [ 103 | '[koa-node-resolve] Transformed 1 module specifier(s) in "/my-module.js"', 104 | '[koa-node-resolve] Transformed 1 module specifier(s) in "/"', 105 | ]); 106 | }); 107 | }); 108 | 109 | test('nodeResolve middleware ignores unresolvable specifiers', async (t) => { 110 | t.plan(2); 111 | const logger = testLogger(); 112 | createAndServe( 113 | { 114 | middleware: [nodeResolve({root: fixturesPath, logger})], 115 | routes: { 116 | '/my-module.js': ` 117 | import * as wubbleFlurp from 'wubble-flurp'; 118 | `, 119 | '/my-page.html': ` 120 | 123 | `, 124 | }, 125 | }, 126 | async (server) => { 127 | t.equal( 128 | squeeze((await request(server).get('/my-module.js')).text), 129 | squeeze(` 130 | import * as wubbleFlurp from 'wubble-flurp'; 131 | `), 132 | 'should leave unresolvable specifier in external scripts alone'); 133 | t.equal( 134 | squeeze((await request(server).get('/my-page.html')).text), 135 | squeeze(` 136 | 139 | `), 140 | 'should leave unresolvable specifier in inline scripts alone'); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /src/test/logger.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | 3 | import {leveledLogger, prefixedLogger} from '../support/logger'; 4 | 5 | import {testLogger} from './test-utils'; 6 | 7 | test('leveledLogger', (t) => { 8 | t.plan(4); 9 | const logger = testLogger(); 10 | const wrappedLogger = leveledLogger(logger, 'warn'); 11 | wrappedLogger.debug && wrappedLogger.debug('test debug'); 12 | wrappedLogger.info && wrappedLogger.info('test info'); 13 | wrappedLogger.warn && wrappedLogger.warn('test warn'); 14 | wrappedLogger.error && wrappedLogger.error('test error'); 15 | t.deepEqual(logger.debugs, []); 16 | t.deepEqual(logger.infos, []); 17 | t.deepEqual(logger.warns, [['test warn']]); 18 | t.deepEqual(logger.errors, [['test error']]); 19 | }); 20 | 21 | test('prefixedLogger', (t) => { 22 | t.plan(4); 23 | const logger = testLogger(); 24 | const wrappedLogger = prefixedLogger('[yo]', logger); 25 | wrappedLogger.debug && wrappedLogger.debug('test debug'); 26 | wrappedLogger.info && wrappedLogger.info('test info'); 27 | wrappedLogger.warn && wrappedLogger.warn('test warn'); 28 | wrappedLogger.error && wrappedLogger.error('test error'); 29 | t.deepEqual(logger.debugs, [['[yo]', 'test debug']]); 30 | t.deepEqual(logger.infos, [['[yo]', 'test info']]); 31 | t.deepEqual(logger.warns, [['[yo]', 'test warn']]); 32 | t.deepEqual(logger.errors, [['[yo]', 'test error']]); 33 | }); 34 | -------------------------------------------------------------------------------- /src/test/path-utils.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2020 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | import test from 'tape'; 15 | 16 | import {dirname, resolvePathPreserveTrailingSlash} from '../support/path-utils'; 17 | 18 | test('dirname returns portion of path representing directory', (t) => { 19 | t.plan(2); 20 | t.equal( 21 | dirname('/a/b/c'), 22 | '/a/b/', 23 | 'should treat lack of trailing slash as file'); 24 | t.equal( 25 | dirname('/a/b/c/'), 26 | '/a/b/c/', 27 | 'should treat segment before trailing slash as directory name'); 28 | }); 29 | 30 | test('resolvePathPreserveTrailingSlash may return trailing slash', (t) => { 31 | t.plan(3); 32 | t.equal( 33 | resolvePathPreserveTrailingSlash('/a/b', 'c/'), 34 | '/a/b/c/', 35 | 'should contain trailing slash when destination has trailing slash'); 36 | t.equal( 37 | resolvePathPreserveTrailingSlash('/a/b', 'c'), 38 | '/a/b/c', 39 | 'should not contain trailing slash if destination does not have trailing slash'); 40 | t.equal( 41 | resolvePathPreserveTrailingSlash('/a/b', ''), 42 | '/a/b/', 43 | 'should contain trailing slash if destination is current directory'); 44 | }); -------------------------------------------------------------------------------- /src/test/resolve-node-specifier.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2019 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | import test from 'tape'; 15 | 16 | import {resolvePathPreserveTrailingSlash} from '../support/path-utils'; 17 | import {resolveNodeSpecifier} from '../support/resolve-node-specifier'; 18 | import {testLogger} from './test-utils'; 19 | 20 | const logger = testLogger(); 21 | const fixturesPath = 22 | resolvePathPreserveTrailingSlash(__dirname, '../../test/fixtures/'); 23 | const resolve = (specifier: string): string => 24 | resolveNodeSpecifier(fixturesPath, specifier, logger); 25 | 26 | test('resolveNodeSpecifier resolves package name', (t) => { 27 | t.plan(3); 28 | t.equal( 29 | resolve('x'), 30 | './node_modules/x/main.js', 31 | 'should resolve to `package.json` "main"'); 32 | t.equal( 33 | resolve('y'), 34 | './node_modules/y/jsnext.js', 35 | 'should resolve to `package.json` "jsnext:main"'); 36 | t.equal( 37 | resolve('z'), 38 | './node_modules/z/module.js', 39 | 'should resolve to `package.json` "module"'); 40 | }); 41 | 42 | test('resolveNodeSpecifier resolves extension-less module subpath', (t) => { 43 | t.plan(3); 44 | t.equal( 45 | resolve('z/jsnext'), 46 | './node_modules/z/jsnext.js', 47 | 'should resolve to `.js` extension'); 48 | t.equal( 49 | resolve('z/package'), 50 | './node_modules/z/package.json', 51 | 'should resolve to `.json` extension'); 52 | t.equal( 53 | resolve('z/binary-file'), 54 | './node_modules/z/binary-file.node', 55 | 'should resolve to `.node` extension'); 56 | }); 57 | 58 | test('resolveNodeSpecifier resolves extension-less relative path', (t) => { 59 | t.plan(3); 60 | t.equal( 61 | resolve('./node_modules/z/jsnext'), 62 | './node_modules/z/jsnext.js', 63 | 'should resolve to `.js` extension'); 64 | t.equal( 65 | resolve('./node_modules/z/package'), 66 | './node_modules/z/package.json', 67 | 'should resolve to `.json` extension'); 68 | t.equal( 69 | resolve('./node_modules/z/binary-file'), 70 | './node_modules/z/binary-file.node', 71 | 'should resolve to `.node` extension'); 72 | }); 73 | 74 | test('resolveNodeSpecifier resolves relative paths', (t) => { 75 | t.plan(5); 76 | t.equal( 77 | resolve('./some-file.js'), 78 | './some-file.js', 79 | 'should resolve relative path to non-package file'); 80 | t.equal( 81 | resolve('./non-existing-file.js'), 82 | './non-existing-file.js', 83 | 'should resolve a relative path specifier when the file does not exist'); 84 | t.equal( 85 | resolve('./node_modules/x/main.js'), 86 | './node_modules/x/main.js', 87 | 'should resolve relative path to existing main file'); 88 | t.equal( 89 | resolve('./node_modules/y/main.js'), 90 | './node_modules/y/main.js', 91 | 'should resolve relative path to existing jsnext:main file'); 92 | t.equal( 93 | resolve('./node_modules/z/main.js'), 94 | './node_modules/z/main.js', 95 | 'should resolve relative path to existing module file'); 96 | }); 97 | 98 | test('resolveNodeSpecifier resolves modules without import/export', (t) => { 99 | t.plan(1); 100 | t.equal( 101 | resolve('no-imports-or-exports'), 102 | './node_modules/no-imports-or-exports/main.js', 103 | 'should resolve package which contains non-module main.js'); 104 | }); 105 | -------------------------------------------------------------------------------- /src/test/test-utils.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2019 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | import test from 'tape'; 15 | 16 | import {squeeze} from './test-utils'; 17 | 18 | test('squeeze will not inject newlines where no-spaces exist', (t) => { 19 | t.plan(1); 20 | t.equal(squeeze('

Hello

'), '

Hello

'); 21 | }); 22 | 23 | test('squeeze will shrink multiple spaces to single spaces', (t) => { 24 | t.plan(1); 25 | t.equal(squeeze('

Hello

'), '

\nHello\n

'); 26 | }); 27 | -------------------------------------------------------------------------------- /src/test/test-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2019 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | import {Server} from 'http'; 15 | import Koa from 'koa'; 16 | import route from 'koa-route'; 17 | import {Readable} from 'stream'; 18 | 19 | import {Logger} from '../support/logger'; 20 | 21 | export type AppOptions = { 22 | middleware?: Koa.Middleware[], 23 | routes?: {[key: string]: string|Function}, 24 | }; 25 | 26 | export const createApp = (options: AppOptions): Koa => { 27 | const app = new Koa(); 28 | const {middleware, routes} = options; 29 | if (middleware) { 30 | for (const m of middleware) { 31 | app.use(m); 32 | } 33 | } 34 | if (routes) { 35 | for (const key of Object.keys(routes)) { 36 | const value = routes[key]; 37 | app.use(route.get(key, (ctx) => { 38 | if (key.endsWith('.js')) { 39 | ctx.type = 'js'; 40 | } else { 41 | // Assume everything else is html. 42 | ctx.type = 'html'; 43 | } 44 | // Make our body a stream, like koa static would do, to make sure we 45 | // aren't, for example, consuming the body streams. 46 | const stream = new Readable(); 47 | stream.push(value); 48 | stream.push(null); 49 | ctx.body = stream; 50 | })); 51 | } 52 | } 53 | return app; 54 | }; 55 | 56 | export const createAndServe = 57 | async (options: AppOptions, callback: (server: Server) => void) => 58 | serveApp(createApp(options), callback); 59 | 60 | export const serveApp = 61 | async (app: Koa, callback: (server: Server) => void) => { 62 | const port = process.env.PORT || 3000; 63 | const server = 64 | app.listen(port).on('error', (e) => `ERROR: ${console.log(e)}`); 65 | await callback(server); 66 | await server.close(); 67 | }; 68 | 69 | export type TestLogger = Logger&{ 70 | debugs: unknown[][], 71 | infos: unknown[][], 72 | errors: unknown[][], 73 | warns: unknown[][] 74 | }; 75 | 76 | export const testLogger = (): TestLogger => { 77 | const logger: TestLogger = { 78 | debugs: [], 79 | debug: (...args: unknown[]) => logger.debugs.push(args), 80 | infos: [], 81 | info: (...args: unknown[]) => logger.infos.push(args), 82 | errors: [], 83 | error: (...args: unknown[]) => logger.errors.push(args), 84 | warns: [], 85 | warn: (...args: unknown[]) => logger.warns.push(args) 86 | }; 87 | return logger; 88 | }; 89 | 90 | export const squeeze = (html: string): string => html.replace(/\s+/mg, ' ') 91 | .replace(/>\s<') 92 | .replace(/>\s/g, '>\n') 93 | .replace(/\s { 29 | const baseURL = getBaseURL(ast, url); 30 | getInlineModuleScripts(ast).forEach((scriptTag) => { 31 | const originalJS = getTextContent(scriptTag); 32 | const transformedJS = preserveSurroundingWhitespace( 33 | originalJS, 34 | jsModuleTransform( 35 | originalJS, 36 | (ast) => transformJSModule( 37 | ast, baseURL, specifierTransform, logger))); 38 | setTextContent(scriptTag, transformedJS); 39 | }); 40 | return ast; 41 | }; 42 | 43 | const getBaseURL = (ast: DefaultTreeNode, location: string): string => { 44 | const baseTag = getBaseTag(ast); 45 | if (!baseTag) { 46 | return location; 47 | } 48 | const baseHref = getAttr(baseTag, 'href'); 49 | if (!baseHref) { 50 | return location; 51 | } 52 | return resolveURL(location, baseHref); 53 | }; 54 | 55 | const getBaseTag = (ast: DefaultTreeNode): DefaultTreeNode|undefined => 56 | getTags(ast, 'base').shift(); 57 | 58 | const getInlineModuleScripts = (ast: DefaultTreeNode): DefaultTreeNode[] => 59 | getTags(ast, 'script') 60 | .filter( 61 | (node) => 62 | getAttr(node, 'type') === 'module' && !getAttr(node, 'src')); 63 | 64 | const getTags = (ast: DefaultTreeNode, name: string): DefaultTreeNode[] => 65 | nodeWalkAll(ast, (node) => node.nodeName === name); 66 | -------------------------------------------------------------------------------- /src/transform-js-module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2019 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | import traverse from '@babel/traverse'; 15 | import {NodePath} from '@babel/traverse'; 16 | import {CallExpression, ExportAllDeclaration, ExportNamedDeclaration, ImportDeclaration, isImport, isStringLiteral, Node, StringLiteral} from '@babel/types'; 17 | 18 | import {SpecifierTransform} from './koa-module-specifier-transform'; 19 | import {Logger} from './support/logger'; 20 | 21 | export const transformJSModule = 22 | (ast: Node, 23 | url: string, 24 | specifierTransform: SpecifierTransform, 25 | logger: Logger): Node => { 26 | const importExportDeclaration = { 27 | enter(path: NodePath) { 29 | if (path.node && path.node.source && 30 | isStringLiteral(path.node.source)) { 31 | const specifier = path.node.source.value; 32 | const transformedSpecifier = 33 | specifierTransform(url, specifier, logger); 34 | if (typeof transformedSpecifier === 'undefined') { 35 | return; 36 | } 37 | path.node.source.value = transformedSpecifier; 38 | } 39 | } 40 | }; 41 | traverse(ast, { 42 | ImportDeclaration: importExportDeclaration, 43 | ExportAllDeclaration: importExportDeclaration, 44 | ExportNamedDeclaration: importExportDeclaration, 45 | CallExpression: { 46 | enter(path: NodePath) { 47 | if (path.node && path.node.callee && isImport(path.node.callee) && 48 | path.node.arguments.length === 1 && 49 | isStringLiteral(path.node.arguments[0])) { 50 | const argument = path.node.arguments[0] as StringLiteral; 51 | const specifier = argument.value; 52 | const transformedSpecifier = 53 | specifierTransform(url, specifier, logger); 54 | if (typeof transformedSpecifier === 'undefined') { 55 | return; 56 | } 57 | argument.value = transformedSpecifier; 58 | } 59 | } 60 | } 61 | }); 62 | return ast; 63 | }; 64 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/no-imports-or-exports/main.js: -------------------------------------------------------------------------------- 1 | window.someGlobalThing('Important side effect!') 2 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/no-imports-or-exports/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "no-imports-or-exports", 3 | "version": "1.0.0", 4 | "main": "main.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/x/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { x: 'x common' } 2 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/x/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "x", 3 | "version": "1.0.0", 4 | "main": "main.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/y/jsnext.js: -------------------------------------------------------------------------------- 1 | export const y = 'y jsnext:main' 2 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/y/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { y: 'y common' } 2 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/y/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "y", 3 | "version": "1.0.0", 4 | "jsnext:main": "jsnext.js", 5 | "main": "main.js" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/z/binary-file.node: -------------------------------------------------------------------------------- 1 | *JUST PRETEND I AM A BINARY FILE* 2 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/z/jsnext.js: -------------------------------------------------------------------------------- 1 | export const z = 'z jsnext:main' 2 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/z/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { z: 'z common' } 2 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/z/module.js: -------------------------------------------------------------------------------- 1 | export const z = 'z module' 2 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/z/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "z", 3 | "version": "1.0.0", 4 | "module": "module.js", 5 | "jsnext:main": "jsnext.js", 6 | "main": "main.js" 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/some-file.js: -------------------------------------------------------------------------------- 1 | console.log('Just some file.') 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "./lib/", 9 | "rootDir": "./src/", 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "moduleResolution": "node", 20 | "esModuleInterop": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "arrow-parens": true, 4 | "class-name": true, 5 | "curly": true, 6 | "indent": [ 7 | true, 8 | "spaces" 9 | ], 10 | "no-any": true, 11 | "prefer-const": true, 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-internal-module": true, 15 | "no-trailing-whitespace": true, 16 | "no-var-keyword": true, 17 | "one-line": [ 18 | true, 19 | "check-open-brace", 20 | "check-whitespace" 21 | ], 22 | "quotemark": [ 23 | true, 24 | "single", 25 | "avoid-escape" 26 | ], 27 | "semicolon": [ 28 | true, 29 | "always" 30 | ], 31 | "trailing-comma": [ 32 | true, 33 | "multiline" 34 | ], 35 | "triple-equals": [ 36 | true, 37 | "allow-null-check" 38 | ], 39 | "typedef-whitespace": [ 40 | true, 41 | { 42 | "call-signature": "nospace", 43 | "index-signature": "nospace", 44 | "parameter": "nospace", 45 | "property-declaration": "nospace", 46 | "variable-declaration": "nospace" 47 | } 48 | ], 49 | "variable-name": [ 50 | true, 51 | "ban-keywords" 52 | ], 53 | "whitespace": [ 54 | true, 55 | "check-branch", 56 | "check-decl", 57 | "check-operator", 58 | "check-separator", 59 | "check-type" 60 | ] 61 | } 62 | } 63 | --------------------------------------------------------------------------------