Skip to content

Commit d9eaf3f

Browse files
author
Sergii.Kliuchnyk
committed
replace error location in exception stack using source map
1 parent 39d0745 commit d9eaf3f

11 files changed

Lines changed: 238 additions & 53 deletions

File tree

README.md

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
# express-engine-jsx
1+
express-engine-jsx
2+
------------------
3+
Full featured template engine for express
24

35
[![Build Status](https://travis-ci.org/redexp/express-engine-jsx.svg?branch=master)](https://travis-ci.org/redexp/express-engine-jsx)
46

5-
Example of `users.jsx` view file
7+
Example of `users.jsx` template file
68
```jsx harmony
79
const Layout = require('./layout');
810

@@ -15,7 +17,7 @@ const Layout = require('./layout');
1517
</Layout>
1618
```
1719

18-
Example of `layout.jsx` view file
20+
Example of `layout.jsx` template file
1921
```jsx harmony
2022
<html lang={lang}>
2123
<head>
@@ -49,14 +51,14 @@ Output html
4951

5052
## How it works
5153

52-
When you render some view, this engine takes `jsx` file like this
54+
When you render some template, this engine takes `jsx` file like this
5355
```jsx harmony
5456
const Layout = require('./layout');
5557

5658
<Layout>
5759
<ul class="users">
58-
{users.map(user => (
59-
<li key={user}>{user.name}</li>
60+
{users.map((user, i) => (
61+
<li key={i}>{user.name}</li>
6062
))}
6163
</ul>
6264
</Layout>
@@ -84,10 +86,10 @@ module.exports = function (props) {
8486
React.createElement(
8587
'ul',
8688
{className: 'users'},
87-
users.map(user => (
89+
users.map((user, i) => (
8890
React.createElement(
8991
'li',
90-
{key: user},
92+
{key: i},
9193
user.name
9294
)
9395
))
@@ -103,7 +105,7 @@ module.exports = function (props) {
103105

104106
and now this component can be rendered to html with `ReactDOM.renderToStaticMarkup()`.
105107

106-
As you can see, each jsx view file returns array of components and standard html attributes are converted to react attributes
108+
As you can see, each jsx template file returns array of components and standard html attributes are converted to react attributes
107109
```html
108110
<div class="first" tabindex="1"></div>
109111
<div class="second" tabindex="2"></div>
@@ -120,6 +122,12 @@ return __components;
120122

121123
## Usage
122124

125+
```
126+
npm i express-engine-jsx react react-dom
127+
```
128+
129+
`react` and `react-dom` are peer dependencies in this package
130+
123131
```javascript
124132
const express = require('express');
125133
const app = express();
@@ -131,15 +139,10 @@ server.engine('jsx', engine);
131139

132140
// optionaly
133141
engine.setOptions({
134-
doctype: "<!DOCTYPE html>\n", // prepended string to every output html
135-
templatePath: '/path/to/template.jsx', // path to base tamplete of component for all jsx templates. Default is "express-engine-jsx/template.jsx",
136-
replace: (html) => {return html}, // Modify final html with this callback
137-
parserOptions: {}, // See https://babeljs.io/docs/en/babel-parser#options
142+
// See options section
138143
});
139144
```
140145

141-
That's it, you no need to do `app.set('views', 'views')` and so on, `attachTo` will do that for you
142-
143146
## API
144147

145148
### engine
@@ -171,8 +174,11 @@ const options = require('express-engine-jsx/options');
171174

172175
Object with optional properties:
173176

177+
* `DEV` - boolean, default `process.env.NODE_ENV !== 'production'`
178+
* `sourceMap` - boolean, default `process.env.NODE_ENV !== 'production'`
174179
* `doctype` - string which will be prepended to output html, default value is `"<!DOCTYPE html>\n"`
175180
* `replace` - function which will take output html (without doctype), and it should return new html
181+
* `addOnChange` - boolean, default `true`. Will add `onChnage={() => false}` to every `<input>` with `value` or `checked` attribute. Used to omit ReactDOM warning about `value` prop without `onChange` handler.
176182
* `templatePath` - path to wrapper of compiled jsx, default value is `express-engine-jsx/template.jsx`. Undefined variable `BODY` will be replaced with your compiled jsx code.
177183
* `parserOptions` - options for [babel.parser](https://babeljs.io/docs/en/babel-parser#options)
178184
* `templateOptions` - options for [babel.template](https://babeljs.io/docs/en/babel-template#options)
@@ -187,8 +193,8 @@ This is a function which you can use as regular `require` but this one can run j
187193

188194
It also can take optional second parameter - `currentWorkingDir` which should be an absolute path to file directory which calls `require` in case when you call `require` from some unusual place like debugger console.
189195

190-
Every compiled jsx file will be cached to `requireJSX.cache` object where key will be path to jsx file and value will be value of `module.exports` inside jsx file, usually react component.
191-
You can delete any key in this cache, requireJSX will recompile jsx file on next call.
196+
Every compiled jsx file will be cached to `requireJSX.cache` object where key will be path to jsx file without extension and value will be object `{moduleExports: ReactComponent|any, map: object|null}`.
197+
You can delete any key in this cache, `requireJSX` will recompile jsx file on next call.
192198

193199
### convert
194200

@@ -202,12 +208,16 @@ Arguments:
202208

203209
* `code` - string of jsx code
204210
* `options`
205-
* `addOnChange` - boolean, default `true`. Will add `onChnage={() => false}` to every `<input>` with `value` or `checked` attribute. Used to omit ReactDOM warning about `value` prop without `onChange` handler.
206-
* `parserOptions` - options for [babel.parser](https://babeljs.io/docs/en/babel-parser#options)
211+
* `path` - string, path to jsx file. Needed only for source map.
212+
* `sourceMap` - boolean, default [options.sourceMap](#options). Generate source map
213+
* `addOnChange` - boolean, default [options.addOnChange](#options)
214+
* `parserOptions` - object, default [options.parserOptions](#options)
207215
* `template` - string of jsx code wrapper. You can pass `false` if you don't want to wrap your code with `template`
208-
* `templatePath` - string, path to jsx code wrapper
209-
* `templateOptions` - options for [babel.template](https://babeljs.io/docs/en/babel-template#options)
216+
* `templatePath` - string, default [options.templatePath](#options)
217+
* `templateOptions` - object, default [options.templateOptions](#options)
210218

219+
If you pass `sourceMap: true` or your `process.env.NODE_ENV !== 'production'` then `convert` will return object `{code: string, map: object}` instead of js code string.
220+
211221
It also has `convert.cache` object for compiled templates where keys are `templatePath` and values are functions created by [babel.template](https://babeljs.io/docs/en/babel-template)
212222

213223
## run

convert.js

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,17 @@ function convert(code, params = {}) {
1616
code = code.toString();
1717
}
1818

19-
let {addOnChange = options.addOnChange, parserOptions, template, templatePath, templateOptions} = params;
20-
21-
var ast = parser.parse(code, parserOptions || options.parserOptions);
19+
let {
20+
path,
21+
sourceMap = options.sourceMap,
22+
addOnChange = options.addOnChange,
23+
parserOptions = options.parserOptions,
24+
template,
25+
templatePath = options.templatePath,
26+
templateOptions = options.templateOptions
27+
} = params;
28+
29+
var ast = parser.parse(code, parserOptions);
2230

2331
traverse(ast, {
2432
enter: function prepare(path) {
@@ -69,16 +77,21 @@ function convert(code, params = {}) {
6977
}
7078
});
7179

72-
if (template === false) return toCode(ast);
80+
if (template === false) {
81+
return toCode(ast, code, {
82+
filename: path,
83+
sourceMap,
84+
});
85+
}
7386

74-
templatePath = templatePath || options.templatePath;
87+
let {cache} = convert;
7588

7689
if (templatePath && !template) {
7790
if (convert.cache[templatePath]) {
7891
template = convert.cache[templatePath];
7992
}
8093
else {
81-
template = fs.readFileSync(templatePath).toString();
94+
template = fs.readFileSync(templatePath);
8295
}
8396
}
8497

@@ -89,8 +102,12 @@ function convert(code, params = {}) {
89102
if (typeof template === 'string') {
90103
template = createTemplate(
91104
template,
92-
templateOptions || options.templateOptions
105+
templateOptions
93106
);
107+
108+
if (templatePath) {
109+
cache[templatePath] = template;
110+
}
94111
}
95112

96113
if (typeof template !== 'function') {
@@ -101,16 +118,22 @@ function convert(code, params = {}) {
101118
BODY: ast.program.body
102119
});
103120

104-
return toCode(t.program(ast));
121+
return toCode(t.program(ast), code, {
122+
filename: path,
123+
sourceMap,
124+
});
105125
}
106126

107-
function toCode(ast) {
108-
var res = babel.transformFromAst(ast, '', {
109-
ast: false,
127+
function toCode(ast, code = '', params = {}) {
128+
const {filename, sourceMap} = params;
129+
130+
var result = babel.transformFromAst(ast, code, {
131+
filename,
132+
sourceMap,
110133
plugins: [
111134
'@babel/plugin-transform-react-jsx'
112135
]
113136
});
114137

115-
return res.code;
138+
return sourceMap ? result : result.code;
116139
}

index.d.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1-
declare function engine(path: string, params: object, cb: (err: null|Error, html: string) => void): void;
2-
declare function engine(path: string, params: object): string;
1+
declare function engine(path: string, params: object, cb: HtmlCallback): void;
2+
declare function engine(path: string, cb: HtmlCallback): void;
3+
declare function engine(path: string, params?: object): string;
4+
5+
type HtmlCallback = (err: null|Error, html: string) => void;
36

47
declare namespace engine {
58
interface Options {
9+
DEV?: boolean,
610
doctype?: string,
711
replace?: (html: string) => string,
812
templatePath?: string,
913
parserOptions?: import('@babel/parser/typings/babel-parser').ParserOptions,
14+
sourceMap?: boolean,
1015
addOnChange?: boolean,
1116
}
1217

1318
interface ConvertOptions {
19+
path?: string,
20+
sourceMap?: boolean,
1421
addOnChange?: boolean,
1522
parserOptions?: import('@babel/parser/typings/babel-parser').ParserOptions,
1623
template?: false | string | Buffer | (({BODY}) => any),
@@ -28,7 +35,7 @@ declare namespace engine {
2835

2936
export function require(path: string, currentWorkingDir?: string): any;
3037

31-
export function convert(code: string|Buffer, options?: ConvertOptions): any;
38+
export function convert(code: string|Buffer, options?: ConvertOptions): string|{code: string, map: null|object};
3239

3340
export function run(code: string|Buffer, options?: RunOptions): any;
3441

index.js

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,59 @@ const Context = require('./Context');
88

99
module.exports = engine;
1010

11-
function engine(path, params, cb) {
11+
function engine(path, params = {}, cb = null) {
12+
if (typeof params === 'function') {
13+
cb = params;
14+
params = {};
15+
}
16+
1217
const Component = requireJSX(path.replace(/\.jsx$/, ''));
1318

1419
const context = {
1520
locals: Object.assign({}, params.locals, params._locals),
1621
settings: params.settings,
1722
};
1823

19-
let html = ReactDOM.renderToStaticMarkup(
20-
React.createElement(Context.Provider, {value: context},
21-
React.createElement(Component, params)
22-
)
23-
);
24+
try {
25+
var html = ReactDOM.renderToStaticMarkup(
26+
React.createElement(Context.Provider, {value: context},
27+
React.createElement(Component, params)
28+
)
29+
);
30+
}
31+
catch (err) {
32+
if (err && typeof err.stack === 'string') {
33+
Object.keys(requireJSX.cache).forEach(function (path) {
34+
var {map} = requireJSX.cache[path];
35+
36+
if (!map) return;
37+
38+
let pathJSX = path + '.jsx';
39+
40+
if (!err.stack.includes(pathJSX)) return;
41+
42+
const {SourceMapConsumer} = require('source-map-sync');
43+
44+
SourceMapConsumer.with(map, null, function (consumer) {
45+
err.stack = err.stack.replace(new RegExp(escapeRegexp(pathJSX) + ":(\\d+):(\\d+)", "g"), function (x, l, c) {
46+
let {line, column} = consumer.originalPositionFor({line: Number(l), column: Number(c)});
47+
48+
if (line === null) return x;
49+
50+
return `${pathJSX}:${line}:${column}`;
51+
});
52+
});
53+
});
54+
}
55+
56+
if (cb) {
57+
cb(err);
58+
return;
59+
}
60+
else {
61+
throw err;
62+
}
63+
}
2464

2565
if (options.replace) {
2666
html = options.replace(html);
@@ -38,6 +78,10 @@ function engine(path, params, cb) {
3878
}
3979
}
4080

81+
function escapeRegexp(str) {
82+
return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
83+
}
84+
4185
engine.setOptions = function (params) {
4286
for (let key in params) {
4387
if (options.hasOwnProperty(key)) {

options.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
const {resolve} = require('path');
22

3+
const DEV = process.env.NODE_ENV !== 'production';
4+
35
module.exports = {
6+
DEV,
7+
sourceMap: DEV,
48
parserOptions: {
59
sourceType: "module",
610
strictMode: false,

0 commit comments

Comments
 (0)