Skip to content

Commit aba2657

Browse files
committed
Basic support for markup, style and script preprocessors
Suggestion for #181 and #876
1 parent 72bd23a commit aba2657

File tree

25 files changed

+970
-47
lines changed

25 files changed

+970
-47
lines changed

package.json

+5
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"acorn": "^5.1.1",
4848
"chalk": "^2.0.1",
4949
"codecov": "^2.2.0",
50+
"coffeescript": "^2.0.2",
5051
"console-group": "^0.3.2",
5152
"css-tree": "1.0.0-alpha22",
5253
"eslint": "^4.3.0",
@@ -55,13 +56,16 @@
5556
"estree-walker": "^0.5.1",
5657
"glob": "^7.1.1",
5758
"jsdom": "^11.1.0",
59+
"less": "^2.7.3",
5860
"locate-character": "^2.0.0",
5961
"magic-string": "^0.22.3",
6062
"mocha": "^3.2.0",
6163
"nightmare": "^2.10.0",
6264
"node-resolve": "^1.3.3",
65+
"node-sass": "^4.7.1",
6366
"nyc": "^11.1.0",
6467
"prettier": "^1.7.0",
68+
"pug": "^2.0.0-rc.4",
6569
"reify": "^0.12.3",
6670
"rollup": "^0.48.2",
6771
"rollup-plugin-buble": "^0.15.0",
@@ -74,6 +78,7 @@
7478
"rollup-watch": "^4.3.1",
7579
"source-map": "^0.5.6",
7680
"source-map-support": "^0.4.8",
81+
"stylus": "^0.54.5",
7782
"ts-node": "^3.3.0",
7883
"tslib": "^1.8.0",
7984
"typescript": "^2.6.1"

src/index.ts

+75-5
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import generate from './generators/dom/index';
44
import generateSSR from './generators/server-side-rendering/index';
55
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+
let normalizedOptions = assign({ generate: 'dom', preprocessor: false }, options);
1314
const { onwarn, onerror } = normalizedOptions;
1415
normalizedOptions.onwarn = onwarn
1516
? (warning: Warning) => onwarn(warning, defaultOnwarn)
@@ -34,9 +35,78 @@ function defaultOnerror(error: Error) {
3435
throw error;
3536
}
3637

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

42112
try {
@@ -53,7 +123,7 @@ export function compile(source: string, _options: CompileOptions) {
53123
const compiler = options.generate === 'ssr' ? generateSSR : generate;
54124

55125
return compiler(parsed, source, stylesheet, options);
56-
}
126+
};
57127

58128
export function create(source: string, _options: CompileOptions = {}) {
59129
_options.format = 'eval';
@@ -65,7 +135,7 @@ export function create(source: string, _options: CompileOptions = {}) {
65135
}
66136

67137
try {
68-
return (0,eval)(compiled.code);
138+
return (0, eval)(compiled.code);
69139
} catch (err) {
70140
if (_options.onerror) {
71141
_options.onerror(err);

src/interfaces.ts

+12-1
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;
@@ -60,6 +62,7 @@ export interface CompileOptions {
6062

6163
onerror?: (error: Error) => void;
6264
onwarn?: (warning: Warning) => void;
65+
preprocessor?: ((raw: string) => string) | false ;
6366
}
6467

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

test/preprocess/index.js

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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`,
105+
dom.css);
106+
assert.equal(dom.css.replace(/svelte-\d+/g, 'svelte-xyz'),
107+
expected.css);
108+
}
109+
110+
// verify that the right elements have scoping selectors
111+
if (expected.html !== null) {
112+
const window = env();
113+
114+
// dom
115+
try {
116+
const Component = eval(
117+
`(function () { ${dom.code}; return SvelteComponent; }())`
118+
);
119+
const target = window.document.querySelector('main');
120+
121+
new Component({target, data: config.data});
122+
const html = target.innerHTML;
123+
124+
fs.writeFileSync(`test/preprocess/samples/${dir}/_actual.html`,
125+
html);
126+
127+
assert.equal(
128+
normalizeHtml(window,
129+
html.replace(/svelte-\d+/g, 'svelte-xyz')),
130+
normalizeHtml(window, expected.html)
131+
);
132+
} catch (err) {
133+
console.log(dom.code);
134+
throw err;
135+
}
136+
137+
// ssr
138+
try {
139+
const component = eval(
140+
`(function () { ${ssr.code}; return SvelteComponent; }())`
141+
);
142+
143+
assert.equal(
144+
normalizeHtml(
145+
window,
146+
component.render(config.data).
147+
replace(/svelte-\d+/g, 'svelte-xyz')
148+
),
149+
normalizeHtml(window, expected.html)
150+
);
151+
} catch (err) {
152+
console.log(ssr.code);
153+
throw err;
154+
}
155+
}
156+
}).catch(error => {
157+
throw error;
158+
});
159+
});
160+
});
161+
});
162+
163+
function read(file) {
164+
try {
165+
return fs.readFileSync(file, 'utf-8');
166+
} catch (err) {
167+
return null;
168+
}
169+
}
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+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<h1>Hello foo!</h1>
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>
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+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[foo][svelte-xyz]{color:red}[baz][svelte-xyz]{color:blue}

0 commit comments

Comments
 (0)