Skip to content

Commit 04384d8

Browse files
committed
Basic support for markup, style and script preprocessors
Suggestion for #181 and #876
1 parent 14b27b7 commit 04384d8

File tree

25 files changed

+964
-50
lines changed

25 files changed

+964
-50
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"acorn": "^5.1.1",
4747
"chalk": "^2.0.1",
4848
"codecov": "^2.2.0",
49+
"coffeescript": "^2.0.2",
4950
"console-group": "^0.3.2",
5051
"css-tree": "1.0.0-alpha22",
5152
"eslint": "^4.3.0",
@@ -54,13 +55,16 @@
5455
"estree-walker": "^0.5.1",
5556
"glob": "^7.1.1",
5657
"jsdom": "^11.1.0",
58+
"less": "^2.7.3",
5759
"locate-character": "^2.0.0",
5860
"magic-string": "^0.22.3",
5961
"mocha": "^3.2.0",
6062
"nightmare": "^2.10.0",
6163
"node-resolve": "^1.3.3",
64+
"node-sass": "^4.7.1",
6265
"nyc": "^11.1.0",
6366
"prettier": "^1.7.0",
67+
"pug": "^2.0.0-rc.4",
6468
"reify": "^0.12.3",
6569
"rollup": "^0.48.2",
6670
"rollup-plugin-buble": "^0.15.0",
@@ -73,6 +77,7 @@
7377
"rollup-watch": "^4.3.1",
7478
"source-map": "^0.5.6",
7579
"source-map-support": "^0.4.8",
80+
"stylus": "^0.54.5",
7681
"ts-node": "^3.3.0",
7782
"tslib": "^1.8.0",
7883
"typescript": "^2.6.1"

src/index.ts

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ import parse from './parse/index';
22
import validate from './validate/index';
33
import generate from './generators/dom/index';
44
import generateSSR from './generators/server-side-rendering/index';
5-
import { assign } from './shared/index.js';
5+
import {assign} from './shared/index.js';
66
import Stylesheet from './css/Stylesheet';
7-
import { Parsed, CompileOptions, Warning } from './interfaces';
7+
import {Parsed, CompileOptions, Warning, PreprocessOptions, Preprocessor} from './interfaces';
8+
import {SourceMap} from 'magic-string';
89

910
const version = '__VERSION__';
1011

1112
function normalizeOptions(options: CompileOptions): CompileOptions {
12-
let normalizedOptions = assign({ generate: 'dom' }, options);
13-
const { onwarn, onerror } = normalizedOptions;
13+
let normalizedOptions = assign({generate: 'dom', preprocessor: false}, options);
14+
const {onwarn, onerror} = normalizedOptions;
1415
normalizedOptions.onwarn = onwarn
1516
? (warning: Warning) => onwarn(warning, defaultOnwarn)
1617
: defaultOnwarn;
@@ -34,9 +35,77 @@ function defaultOnerror(error: Error) {
3435
throw error;
3536
}
3637

38+
function _parseAttributeValue(value: string | boolean) {
39+
if (value === 'true' || value === 'false') {
40+
return value === 'true';
41+
}
42+
return (<string>value).replace(/"/ig, '');
43+
}
44+
45+
function _parseStyleAttributes(str: string) {
46+
const attrs = {};
47+
str.split(/\s+/).filter(Boolean).forEach(attr => {
48+
const [name, value] = attr.split('=');
49+
attrs[name] = _parseAttributeValue(value);
50+
});
51+
return attrs;
52+
}
53+
54+
async function _doPreprocess(source, type: 'script' | 'style', preprocessor: Preprocessor) {
55+
const exp = new RegExp(`<${type}([\\S\\s]*?)>([\\S\\s]*?)<\\/${type}>`, 'ig');
56+
const match = exp.exec(source);
57+
if (match) {
58+
const attributes: Record<string, string | boolean> = _parseStyleAttributes(match[1]);
59+
const content: string = match[2];
60+
const processed: { code: string, map?: SourceMap | string } = await preprocessor({
61+
content,
62+
attributes
63+
});
64+
return source.replace(content, processed.code || content);
65+
}
66+
}
67+
68+
export async function preprocess(source: string, options: PreprocessOptions) {
69+
const {markup, style, script} = options;
70+
if (!!markup) {
71+
try {
72+
const processed: { code: string, map?: SourceMap | string } = await markup({content: source});
73+
source = processed.code;
74+
} catch (error) {
75+
defaultOnerror(error);
76+
}
77+
}
78+
79+
if (!!style) {
80+
try {
81+
source = await _doPreprocess(source, 'style', style);
82+
} catch (error) {
83+
defaultOnerror(error);
84+
}
85+
}
86+
87+
if (!!script) {
88+
try {
89+
source = await _doPreprocess(source, 'script', script);
90+
} catch (error) {
91+
defaultOnerror(error);
92+
}
93+
}
94+
95+
return {
96+
// TODO return separated output, in future version where svelte.compile supports it:
97+
// style: { code: styleCode, map: styleMap },
98+
// script { code: scriptCode, map: scriptMap },
99+
// markup { code: markupCode, map: markupMap },
100+
101+
toString() {
102+
return source;
103+
}
104+
};
105+
}
106+
37107
export function compile(source: string, _options: CompileOptions) {
38108
const options = normalizeOptions(_options);
39-
40109
let parsed: Parsed;
41110

42111
try {
@@ -53,7 +122,7 @@ export function compile(source: string, _options: CompileOptions) {
53122
const compiler = options.generate === 'ssr' ? generateSSR : generate;
54123

55124
return compiler(parsed, source, stylesheet, options);
56-
}
125+
};
57126

58127
export function create(source: string, _options: CompileOptions = {}) {
59128
_options.format = 'eval';
@@ -65,7 +134,7 @@ export function create(source: string, _options: CompileOptions = {}) {
65134
}
66135

67136
try {
68-
return (0,eval)(compiled.code);
137+
return (0, eval)(compiled.code);
69138
} catch (err) {
70139
if (_options.onerror) {
71140
_options.onerror(err);
@@ -76,4 +145,4 @@ export function create(source: string, _options: CompileOptions = {}) {
76145
}
77146
}
78147

79-
export { parse, validate, version as VERSION };
148+
export {parse, validate, version as VERSION};

src/interfaces.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {SourceMap} from 'magic-string';
2+
13
export interface Node {
24
start: number;
35
end: number;
@@ -59,6 +61,7 @@ export interface CompileOptions {
5961

6062
onerror?: (error: Error) => void;
6163
onwarn?: (warning: Warning) => void;
64+
preprocessor?: ((raw: string) => string) | false ;
6265
}
6366

6467
export interface GenerateOptions {
@@ -77,4 +80,12 @@ export interface Visitor {
7780
export interface CustomElementOptions {
7881
tag?: string;
7982
props?: string[];
80-
}
83+
}
84+
85+
export interface PreprocessOptions {
86+
markup?: (options: {content: string}) => { code: string, map?: SourceMap | string };
87+
style?: Preprocessor;
88+
script?: Preprocessor;
89+
}
90+
91+
export type Preprocessor = (options: {content: string, attributes: Record<string, string | boolean>}) => { code: string, map?: SourceMap | string };

test/preprocess/index.js

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import assert from 'assert';
2+
import * as fs from 'fs';
3+
import {parse} from 'acorn';
4+
import {addLineNumbers, env, normalizeHtml, svelte} from '../helpers.js';
5+
6+
function tryRequire(file) {
7+
try {
8+
const mod = require(file);
9+
return mod.default || mod;
10+
} catch (err) {
11+
if (err.code !== 'MODULE_NOT_FOUND') throw err;
12+
return null;
13+
}
14+
}
15+
16+
function normalizeWarning(warning) {
17+
warning.frame = warning.frame.replace(/^\n/, '').
18+
replace(/^\t+/gm, '').
19+
replace(/\s+$/gm, '');
20+
delete warning.filename;
21+
delete warning.toString;
22+
return warning;
23+
}
24+
25+
function checkCodeIsValid(code) {
26+
try {
27+
parse(code);
28+
} catch (err) {
29+
console.error(addLineNumbers(code));
30+
throw new Error(err.message);
31+
}
32+
}
33+
34+
describe('preprocess', () => {
35+
fs.readdirSync('test/preprocess/samples').forEach(dir => {
36+
if (dir[0] === '.') return;
37+
38+
// add .solo to a sample directory name to only run that test
39+
const solo = /\.solo/.test(dir);
40+
const skip = /\.skip/.test(dir);
41+
42+
if (solo && process.env.CI) {
43+
throw new Error('Forgot to remove `solo: true` from test');
44+
}
45+
46+
(solo ? it.only : skip ? it.skip : it)(dir, () => {
47+
const config = tryRequire(`./samples/${dir}/_config.js`) || {};
48+
const input = fs.existsSync(`test/preprocess/samples/${dir}/input.pug`) ?
49+
fs.readFileSync(`test/preprocess/samples/${dir}/input.pug`,
50+
'utf-8').replace(/\s+$/, '') :
51+
fs.readFileSync(`test/preprocess/samples/${dir}/input.html`,
52+
'utf-8').replace(/\s+$/, '');
53+
54+
svelte.preprocess(input, config).
55+
then(processed => processed.toString()).
56+
then(processed => {
57+
58+
const expectedWarnings = (config.warnings || []).map(
59+
normalizeWarning);
60+
const domWarnings = [];
61+
const ssrWarnings = [];
62+
63+
const dom = svelte.compile(
64+
processed,
65+
Object.assign(config, {
66+
format: 'iife',
67+
name: 'SvelteComponent',
68+
onwarn: warning => {
69+
domWarnings.push(warning);
70+
},
71+
}),
72+
);
73+
74+
const ssr = svelte.compile(
75+
processed,
76+
Object.assign(config, {
77+
format: 'iife',
78+
generate: 'ssr',
79+
name: 'SvelteComponent',
80+
onwarn: warning => {
81+
ssrWarnings.push(warning);
82+
},
83+
}),
84+
);
85+
86+
// check the code is valid
87+
checkCodeIsValid(dom.code);
88+
checkCodeIsValid(ssr.code);
89+
90+
assert.equal(dom.css, ssr.css);
91+
92+
assert.deepEqual(
93+
domWarnings.map(normalizeWarning),
94+
ssrWarnings.map(normalizeWarning),
95+
);
96+
assert.deepEqual(domWarnings.map(normalizeWarning), expectedWarnings);
97+
98+
const expected = {
99+
html: read(`test/preprocess/samples/${dir}/expected.html`),
100+
css: read(`test/preprocess/samples/${dir}/expected.css`),
101+
};
102+
103+
if (expected.css !== null) {
104+
fs.writeFileSync(`test/preprocess/samples/${dir}/_actual.css`, dom.css);
105+
assert.equal(dom.css.replace(/svelte-\d+/g, 'svelte-xyz'),
106+
expected.css);
107+
}
108+
109+
// verify that the right elements have scoping selectors
110+
if (expected.html !== null) {
111+
const window = env();
112+
113+
// dom
114+
try {
115+
const Component = eval(
116+
`(function () { ${dom.code}; return SvelteComponent; }())`
117+
);
118+
const target = window.document.querySelector('main');
119+
120+
new Component({ target, data: config.data });
121+
const html = target.innerHTML;
122+
123+
fs.writeFileSync(`test/preprocess/samples/${dir}/_actual.html`, html);
124+
125+
assert.equal(
126+
normalizeHtml(window, html.replace(/svelte-\d+/g, 'svelte-xyz')),
127+
normalizeHtml(window, expected.html)
128+
);
129+
} catch (err) {
130+
console.log(dom.code);
131+
throw err;
132+
}
133+
134+
// ssr
135+
try {
136+
const component = eval(
137+
`(function () { ${ssr.code}; return SvelteComponent; }())`
138+
);
139+
140+
assert.equal(
141+
normalizeHtml(
142+
window,
143+
component.render(config.data).replace(/svelte-\d+/g, 'svelte-xyz')
144+
),
145+
normalizeHtml(window, expected.html)
146+
);
147+
} catch (err) {
148+
console.log(ssr.code);
149+
throw err;
150+
}
151+
}
152+
}).catch(error => {
153+
throw error
154+
});
155+
});
156+
});
157+
});
158+
159+
function read(file) {
160+
try {
161+
return fs.readFileSync(file, 'utf-8');
162+
} catch (err) {
163+
return null;
164+
}
165+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as CoffeeScript from 'coffeescript';
2+
3+
export default {
4+
cascade: false,
5+
script: ({content, attributes}) => {
6+
if (attributes.type !== 'text/coffeescript') {
7+
return {code: content};
8+
}
9+
10+
return new Promise((fulfil, reject) => {
11+
try {
12+
const code = CoffeeScript.compile(content, {});
13+
fulfil({code});
14+
} catch (error) {
15+
reject(error);
16+
}
17+
});
18+
},
19+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<h1>Hello foo!</h1>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<h1>Hello {{name}}!</h1>
2+
3+
<script type="text/coffeescript">
4+
export default {
5+
data: () ->
6+
name: 'foo'
7+
};
8+
</script>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as pug from 'pug';
2+
3+
export default {
4+
cascade: false,
5+
markup: ({content}) => {
6+
return new Promise((fulfil, reject) => {
7+
try {
8+
const code = pug.render(content);
9+
fulfil({code});
10+
} catch (error) {
11+
reject(error);
12+
}
13+
});
14+
},
15+
};

0 commit comments

Comments
 (0)