Skip to content
This repository has been archived by the owner on Jul 30, 2018. It is now read-only.

Commit

Permalink
Add source maps for generated code (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
kitsonk authored May 12, 2017
1 parent 095ecb8 commit b5f7a10
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 34 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@types/grunt": "^0.4.21",
"@types/node": "^7.0.12",
"@types/sinon": "^2.1.2",
"@types/source-map": "^0.5.0",
"@types/yargs": "^6.6.0",
"chalk": "^1.1.3",
"codecov.io": "0.1.6",
Expand Down Expand Up @@ -58,6 +59,7 @@
"@dojo/loader": "beta1",
"@dojo/routing": "beta1",
"@dojo/shim": "beta1",
"monaco-editor": "^0.8.3"
"monaco-editor": "^0.8.3",
"source-map": "^0.5.6"
}
}
70 changes: 48 additions & 22 deletions src/Runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import { ProjectFileType } from '@dojo/cli-export-project/interfaces/project.jso
import Evented from '@dojo/core/Evented';
import { createHandle } from '@dojo/core/lang';
import project from './project';
import * as base64 from './support/base64';
import DOMParser from './support/DOMParser';
import { wrapCode } from './support/sourceMap';

export interface GetDocOptions {
css?: { name: string; text: string; }[];
bodyAttributes?: { [attr: string]: string; };
dependencies: { [pkg: string]: string; };
loaderSrc: string;
html?: string;
modules: { [mid: string]: string; };
modules: { [mid: string]: { code: string; map: string; } };
scripts?: string[];
}

Expand Down Expand Up @@ -44,7 +46,7 @@ function docSrc(
loaderSrc: string,
dependencies: { [pkg: string]: string; },
packages: string[],
modules: { [mid: string]: string }
modules: { [mid: string]: { code: string, map: string } }
): string {
const paths: string[] = [];
for (const pkg in dependencies) {
Expand All @@ -56,16 +58,28 @@ function docSrc(
${packages.join(',\n\t\t\t\t\t\t\t')}
]`;

let modulesText = `var cache = {\n`;
let modulesText = '';
for (const mid in modules) {
modulesText += `\t'${mid}': function () {\n${modules[mid]}\n},\n`;
/* inject each source module as its own <script> block */
const filename = mid + '.js';
modulesText += '<script>';
const source = wrapCode(`cache['${mid}'] = function () {\n`, modules[mid], '\n};\n');
modulesText += source.code;
/* if we have a sourcemap then we encode it and add it to the page */
if (modules[mid].map) {
const map = source.map.toJSON();
map.file = filename;
modulesText += `//# sourceMappingURL=data:application/json;base64,${base64.encode(JSON.stringify(map))}\n`;
}
/* adding the sourceURL gives debuggers a "name" for this block of code */
modulesText += `//# sourceURL=${filename}\n`;
modulesText += '</script>\n';
}
modulesText += `};\nrequire.cache(cache);\n/* workaround for dojo/loader#124 */\nrequire.cache({});\n`;

const cssText = css.map(({ name, text }) => {
/* when external CSS is brought into a document, its URL URIs might not be encoded, this will encode them */
const encoded = text.replace(/url\(['"]?(.*?)["']?\)/ig, (match, p1: string) => `url('${encodeURI(p1)}')`);
return `<style>\n/* from: ${name} */\n\n${encoded}\n</style>`;
return `<style>\n${encoded}\n</style>`;
}).join('\n');

let scriptsText = '';
Expand Down Expand Up @@ -213,18 +227,26 @@ export default class Runner extends Evented {
<body${bodyAttributes}>
${html}
<script src="${loaderSrc}"></script>
<script>
require.config({
paths: ${dependencies},
packages: ${getPackages(dependencies)}
});
${modules}
require([ 'tslib', '@dojo/core/request', '../support/providers/amdRequire' ], function () {
var request = require('@dojo/core/request').default;
var getProvider = require('../support/providers/amdRequire').default;
request.setDefaultProvider(getProvider(require));
require([ 'src/main' ], function () { });
});
<script>require.config({
paths: ${dependencies},
packages: ${getPackages(dependencies)}
});
var cache = {};
//# sourceURL=web-editor/config.js
</script>
${modules}
<script>require.cache(cache);
/* workaround for dojo/loader#124 */
require.cache({});
require([ 'tslib', '@dojo/core/request', '../support/providers/amdRequire' ], function () {
var request = require('@dojo/core/request').default;
var getProvider = require('../support/providers/amdRequire').default;
request.setDefaultProvider(getProvider(require));
require([ 'src/main' ], function () { });
});
//# sourceURL=web-editor/bootstrap.js
</script>
</body>
</html>`;
Expand All @@ -241,11 +263,15 @@ export default class Runner extends Evented {
const program = await project.emit();

const modules = program
.filter(({ type }) => type === ProjectFileType.JavaScript)
.reduce((map, { name, text }) => {
map[name.replace(/\.js$/, '')] = text;
.filter(({ type }) => type === ProjectFileType.JavaScript || type === ProjectFileType.SourceMap)
.reduce((map, { name, text, type }) => {
const mid = name.replace(/\.js(?:\.map)?$/, '');
if (!(mid in map)) {
map[mid] = { code: '', map: '' };
}
map[mid][type === ProjectFileType.JavaScript ? 'code' : 'map'] = text;
return map;
}, {} as { [mid: string]: string });
}, {} as { [mid: string]: { code: string; map: string; } });

const css = program
.filter(({ type }) => type === ProjectFileType.CSS)
Expand Down
3 changes: 2 additions & 1 deletion src/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ interface NodeRequire {
require.config({
paths: {
'vs': '../../../node_modules/monaco-editor/min/vs',
'@dojo': '../../../node_modules/@dojo'
'@dojo': '../../../node_modules/@dojo',
'source-map': '../../../node_modules/source-map/dist/source-map.min'
},
packages: [
{ name: 'src', location: '../../..' }
Expand Down
27 changes: 27 additions & 0 deletions src/support/base64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import global from '@dojo/core/global';
import has, { add as hasAdd } from '@dojo/has/has';

hasAdd('btoa', 'btoa' in global);
hasAdd('atob', 'atob' in global);

/**
* Take a string encoded in base64 and decode it
* @param encodedString The base64 encoded string
*/
export const decode: (encodedString: string) => string = has('atob') ? function (encodedString: string) {
/* this allows for utf8 characters to be decoded properly */
return decodeURIComponent(Array.prototype.map.call(atob(encodedString), (char: string) => '%' + ('00' + char.charCodeAt(0).toString(16)).slice(-2)).join(''));
} : function (encodedString: string): string {
return new Buffer(encodedString.toString(), 'base64').toString('utf8');
};

/**
* Take a string and encode it to base64
* @param rawString The string to encode
*/
export const encode: (rawString: string) => string = has('btoa') ? function (decodedString: string) {
/* this allows for utf8 characters to be encoded properly */
return btoa(encodeURIComponent(decodedString).replace(/%([0-9A-F]{2})/g, (match, code: string) => String.fromCharCode(Number('0x' + code))));
} : function (rawString: string): string {
return new Buffer(rawString.toString(), 'utf8').toString('base64');
};
9 changes: 8 additions & 1 deletion src/support/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,14 @@ export async function getEmit(...files: ProjectFile[]): Promise<EmitFile[]> {
for (let i = 0; i < files.length; i++) {
const file = files[i];
mappedClasses = undefined;
const result = await processor.process(file.text);
const result = await processor.process(`/* from: ${file.name} */\n\n` + file.text, {
from: file.name,
map: {
sourcesContent: true
}
});

/* add emitted css text */
emitFiles.push({
name: file.name,
text: result.css,
Expand Down
23 changes: 23 additions & 0 deletions src/support/sourceMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CodeWithSourceMap, SourceMapConsumer, SourceNode } from 'source-map';

const SOURCE_MAP_REGEX = /(?:\/{2}[#@]{1,2}|\/\*)\s+sourceMappingURL\s*=\s*(data:(?:[^;]+;)+base64,)?(\S+)(?:\n\s*)?$/;

/**
* Wrap code, which has a source map, with a preamble and a postscript and return the wrapped code with an updated
* map.
* @param preamble A string to append before the code
* @param code The code, with an optional source map in string format
* @param postscript A string to append after the code
*/
export function wrapCode(preamble: string, code: { map?: string, code: string }, postscript: string): CodeWithSourceMap {
const result = new SourceNode();
result.add(preamble);
if (code.map) {
result.add(SourceNode.fromStringWithSourceMap(code.code.replace(SOURCE_MAP_REGEX, ''), new SourceMapConsumer(code.map)));
}
else {
result.add(code.code);
}
result.add(postscript);
return result.toStringWithSourceMap();
}
3 changes: 2 additions & 1 deletion tests/intern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const environments = [
{ browserName: 'edge', platform: 'WINDOWS' },
{ browserName: 'firefox', platform: 'WINDOWS' },
{ browserName: 'chrome', platform: 'WINDOWS' },
{ browserName: 'safari', version: '10', platform: 'MAC' },
{ browserName: 'safari', version: '9.1', platform: 'MAC' },
{ browserName: 'iPad', version: '9.1' }
];

Expand All @@ -38,6 +38,7 @@ export const loaderOptions = {
{ name: 'dojo', location: 'node_modules/intern/browser_modules/dojo' },
{ name: 'src', location: 'dev/src' },
{ name: 'sinon', location: 'node_modules/sinon/pkg', main: 'sinon' },
{ name: 'source-map', location: 'node_modules/source-map/dist', main: 'source-map.debug' },
{ name: 'tests', location: 'dev/tests' }
]
};
Expand Down
44 changes: 36 additions & 8 deletions tests/unit/Runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ let projectIndexHtml = '';
let iframe: HTMLIFrameElement;
let runner: UnitUnderTest;

const testJS = `define(["require", "exports"], function (require, exports) {
"use strict";
exports.__esModule = true;
function foo() { console.log('bar'); }
exports.foo = foo;
;
});
`;

const testMap = `{"version":3,"file":"test.js","sourceRoot":"","sources":["test.ts"],"names":[],"mappings":";;;IAAA,iBAAwB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAA7C,kBAA6C;IAAA,CAAC","sourcesContent":["export function foo() { console.log('bar'); };\\n"]}`;

registerSuite({
name: 'Runner',

Expand Down Expand Up @@ -85,7 +96,7 @@ registerSuite({
assert.lengthOf(strings, 1, 'should have the proper number of strings');
const doc = getDocFromString(strings[0]);
const scripts = doc.querySelectorAll('script');
assert.lengthOf(scripts, 2, 'should have had two blocks of script injected');
assert.lengthOf(scripts, 3, 'should have had three blocks of script injected');
const styles = doc.querySelectorAll('style');
assert.lengthOf(styles, 0, 'should have no styles in header');
},
Expand All @@ -98,14 +109,33 @@ registerSuite({
async 'adds modules to run iframe'() {
projectEmit.push({
name: 'src/foo.js',
text: 'define([], function () { console.log("foo"); });',
text: testJS,
type: ProjectFileType.JavaScript
});
await runner.run();
const doc = getDocFromString(getDocumentStrings(iframe)[0]);
const scripts = doc.querySelectorAll('script');
assert.include(scripts[2].text, `cache['src/foo'] = function`, 'should have exported module');
assert.include(scripts[2].text, testJS, 'should include module text');
assert.include(scripts[2].text, '//# sourceURL=src/foo.js', 'should include a source URL');
},

async 'adds modules with source maps to run in iframe'() {
projectEmit.push({
name: 'src/foo.js',
text: testJS,
type: ProjectFileType.JavaScript
});
projectEmit.push({
name: 'src/foo.js.map',
text: testMap,
type: ProjectFileType.SourceMap
});
await runner.run();
const doc = getDocFromString(getDocumentStrings(iframe)[0]);
const scripts = doc.querySelectorAll('script');
assert.include(scripts[1].text, `'src/foo': function`, 'should have exported module');
assert.include(scripts[1].text, 'define([], function () { console.log("foo"); });', 'should include module text');
assert.include(scripts[2].text, testJS, 'should include module text');
assert.include(scripts[2].text, '//# sourceMappingURL=data:application/json;base64,', 'should include an inline sourcemap');
},

async 'adds css to run iframe'() {
Expand All @@ -117,7 +147,6 @@ registerSuite({
await runner.run();
const doc = getDocFromString(getDocumentStrings(iframe)[0]);
const styles = doc.querySelectorAll('style');
assert.include(styles[0].textContent!, '/* from: src/main.css */', 'should have comment added');
assert.include(styles[0].textContent!, 'body { font-size: 48px }', 'should have text added');
},

Expand All @@ -143,7 +172,6 @@ registerSuite({
await runner.run();
const doc = getDocFromString(getDocumentStrings(iframe)[0]);
const styles = doc.querySelectorAll('style');
assert.include(styles[0].textContent!, '/* from: project index */', 'should have comment added');
assert.include(styles[0].textContent!, 'body { font-size: 12px; }', 'should have text added');
assert.include(styles[0].textContent!, '.foo { font-size: 24px; }', 'should have text added');
assert.include(styles[0].textContent!, '.bar { font-size: 72px; }', 'should have text added');
Expand Down Expand Up @@ -176,7 +204,7 @@ registerSuite({
await runner.run();
const doc = getDocFromString(getDocumentStrings(iframe)[0]);
const scripts = doc.querySelectorAll('script');
assert.lengthOf(scripts, 4, 'should have four script nodes');
assert.lengthOf(scripts, 5, 'should have five script nodes');
assert.isNotTrue(scripts[0].text, 'script node should not have text');
assert.isNotTrue(scripts[1].text, 'script node should not have text');
assert.strictEqual(scripts[0].src, 'http://foo.bar/index.js', 'should have proper src attribute');
Expand Down Expand Up @@ -240,7 +268,7 @@ registerSuite({
});
const doc = getDocFromString(text);
const scripts = doc.querySelectorAll('script');
assert.lengthOf(scripts, 2, 'should have two script blocks');
assert.lengthOf(scripts, 3, 'should have three script blocks');
assert.strictEqual(scripts[0].getAttribute('src'), 'foo.bar.js\n', 'should set proper loader source');
},

Expand Down
2 changes: 2 additions & 0 deletions tests/unit/support/all.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import './base64';
import './css';
import './DOMParser';
import './gists';
import './json';
import './postcss';
import './postcssCssnext';
import './postcssModules';
import './sourceMap';
import './providers/all';
27 changes: 27 additions & 0 deletions tests/unit/support/base64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as registerSuite from 'intern!object';
import * as assert from 'intern/chai!assert';
import * as base64 from '../../../src/support/base64';

registerSuite({
name: 'support/base64',

'encode()': {
'normal string'() {
assert.strictEqual(base64.encode('foo bar baz'), 'Zm9vIGJhciBiYXo=', 'should have encoded properly');
},

'utf8 string'() {
assert.strictEqual(base64.encode('💩😱🦄'), '8J+SqfCfmLHwn6aE', 'should have encoded properly');
}
},

'decode()': {
'normal string'() {
assert.strictEqual(base64.decode('Zm9vIGJhciBiYXo='), 'foo bar baz', 'should have decoded properly');
},

'utf8 string'() {
assert.strictEqual(base64.decode('8J+SqfCfmLHwn6aE'), '💩😱🦄', 'should have decoded properly');
}
}
});
Loading

0 comments on commit b5f7a10

Please sign in to comment.