Skip to content

Commit 50edc47

Browse files
committed
Basic support for style and script preprocessors
Suggestion for #181 and #876
1 parent a717c82 commit 50edc47

File tree

22 files changed

+937
-72
lines changed

22 files changed

+937
-72
lines changed

package.json

Lines changed: 5 additions & 0 deletions
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

Lines changed: 67 additions & 5 deletions
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,70 @@ 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 { style, script } = options;
71+
72+
if (!!style) {
73+
try {
74+
source = await _doPreprocess(source, 'style', style);
75+
} catch (error) {
76+
defaultOnerror(error);
77+
}
78+
}
79+
80+
if (!!script) {
81+
try {
82+
source = await _doPreprocess(source, 'script', script);
83+
} catch (error) {
84+
defaultOnerror(error);
85+
}
86+
}
87+
88+
return {
89+
// TODO return separated output, in future version where svelte.compile supports it:
90+
// style: { code: styleCode, map: styleMap },
91+
// script { code: scriptCode, map: scriptMap },
92+
// markup { code: markupCode, map: markupMap },
93+
94+
toString() {
95+
return source;
96+
}
97+
};
98+
}
99+
37100
export function compile(source: string, _options: CompileOptions) {
38101
const options = normalizeOptions(_options);
39-
40102
let parsed: Parsed;
41103

42104
try {
@@ -53,7 +115,7 @@ export function compile(source: string, _options: CompileOptions) {
53115
const compiler = options.generate === 'ssr' ? generateSSR : generate;
54116

55117
return compiler(parsed, source, stylesheet, options);
56-
}
118+
};
57119

58120
export function create(source: string, _options: CompileOptions = {}) {
59121
_options.format = 'eval';
@@ -65,7 +127,7 @@ export function create(source: string, _options: CompileOptions = {}) {
65127
}
66128

67129
try {
68-
return (0,eval)(compiled.code);
130+
return (0, eval)(compiled.code);
69131
} catch (err) {
70132
if (_options.onerror) {
71133
_options.onerror(err);

src/interfaces.ts

Lines changed: 11 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;
@@ -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,11 @@ export interface Visitor {
7881
export interface CustomElementOptions {
7982
tag?: string;
8083
props?: string[];
81-
}
84+
}
85+
86+
export interface PreprocessOptions {
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: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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.readFileSync(`test/preprocess/samples/${dir}/input.html`,
49+
'utf-8').replace(/\s+$/, '');
50+
51+
svelte.preprocess(input, config).
52+
then(processed => processed.toString()).
53+
then(processed => {
54+
55+
const expectedWarnings = (config.warnings || []).map(
56+
normalizeWarning);
57+
const domWarnings = [];
58+
const ssrWarnings = [];
59+
60+
const dom = svelte.compile(
61+
processed,
62+
Object.assign(config, {
63+
format: 'iife',
64+
name: 'SvelteComponent',
65+
onwarn: warning => {
66+
domWarnings.push(warning);
67+
},
68+
})
69+
);
70+
71+
const ssr = svelte.compile(
72+
processed,
73+
Object.assign(config, {
74+
format: 'iife',
75+
generate: 'ssr',
76+
name: 'SvelteComponent',
77+
onwarn: warning => {
78+
ssrWarnings.push(warning);
79+
},
80+
})
81+
);
82+
83+
// check the code is valid
84+
checkCodeIsValid(dom.code);
85+
checkCodeIsValid(ssr.code);
86+
87+
assert.equal(dom.css, ssr.css);
88+
89+
assert.deepEqual(
90+
domWarnings.map(normalizeWarning),
91+
ssrWarnings.map(normalizeWarning)
92+
);
93+
assert.deepEqual(domWarnings.map(normalizeWarning), expectedWarnings);
94+
95+
const expected = {
96+
html: read(`test/preprocess/samples/${dir}/expected.html`),
97+
css: read(`test/preprocess/samples/${dir}/expected.css`),
98+
};
99+
100+
if (expected.css !== null) {
101+
fs.writeFileSync(`test/preprocess/samples/${dir}/_actual.css`,
102+
dom.css);
103+
assert.equal(dom.css.replace(/svelte-\d+/g, 'svelte-xyz'),
104+
expected.css);
105+
}
106+
107+
// verify that the right elements have scoping selectors
108+
if (expected.html !== null) {
109+
const window = env();
110+
111+
// dom
112+
try {
113+
const Component = eval(
114+
`(function () { ${dom.code}; return SvelteComponent; }())`
115+
);
116+
const target = window.document.querySelector('main');
117+
118+
new Component({target, data: config.data});
119+
const html = target.innerHTML;
120+
121+
fs.writeFileSync(`test/preprocess/samples/${dir}/_actual.html`,
122+
html);
123+
124+
assert.equal(
125+
normalizeHtml(window,
126+
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).
144+
replace(/svelte-\d+/g, 'svelte-xyz')
145+
),
146+
normalizeHtml(window, expected.html)
147+
);
148+
} catch (err) {
149+
console.log(ssr.code);
150+
throw err;
151+
}
152+
}
153+
}).catch(error => {
154+
throw error;
155+
});
156+
});
157+
});
158+
});
159+
160+
function read(file) {
161+
try {
162+
return fs.readFileSync(file, 'utf-8');
163+
} catch (err) {
164+
return null;
165+
}
166+
}
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: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as sass from 'node-sass';
2+
import * as path from 'path';
3+
4+
export default {
5+
cascade: false,
6+
style: ({ content, attributes }) => {
7+
if (attributes.type !== 'text/scss') {
8+
return {code: content};
9+
}
10+
11+
return new Promise((fulfil, reject) => {
12+
sass.render({
13+
data: content,
14+
includePaths: [
15+
path.resolve(__dirname)
16+
]
17+
}, (err, result) => {
18+
if (err) {
19+
reject(err);
20+
} else {
21+
fulfil({ code: result.css.toString(), map: result.map });
22+
}
23+
});
24+
});
25+
}
26+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
$foo-color: red;
2+
$baz-color: blue;
Lines changed: 1 addition & 0 deletions
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)