Skip to content

Commit e401360

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

File tree

25 files changed

+979
-61
lines changed

25 files changed

+979
-61
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: 88 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,71 +4,140 @@ 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
15-
? (warning: Warning) => onwarn(warning, defaultOnwarn)
16+
? (warning: Warning) => onwarn( warning, defaultOnwarn )
1617
: defaultOnwarn;
1718
normalizedOptions.onerror = onerror
18-
? (error: Error) => onerror(error, defaultOnerror)
19+
? (error: Error) => onerror( error, defaultOnerror )
1920
: defaultOnerror;
2021
return normalizedOptions;
2122
}
2223

2324
function defaultOnwarn(warning: Warning) {
24-
if (warning.loc) {
25+
if ( warning.loc ) {
2526
console.warn(
2627
`(${warning.loc.line}:${warning.loc.column}) – ${warning.message}`
2728
); // eslint-disable-line no-console
2829
} else {
29-
console.warn(warning.message); // eslint-disable-line no-console
30+
console.warn( warning.message ); // eslint-disable-line no-console
3031
}
3132
}
3233

3334
function defaultOnerror(error: Error) {
3435
throw error;
3536
}
3637

37-
export function compile(source: string, _options: CompileOptions) {
38-
const options = normalizeOptions(_options);
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+
}
3978

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+
107+
export function compile(source: string, _options: CompileOptions) {
108+
const options = normalizeOptions( _options );
40109
let parsed: Parsed;
41110

42111
try {
43-
parsed = parse(source, options);
112+
parsed = parse( source, options );
44113
} catch (err) {
45-
options.onerror(err);
114+
options.onerror( err );
46115
return;
47116
}
48117

49-
const stylesheet = new Stylesheet(source, parsed, options.filename, options.cascade !== false);
118+
const stylesheet = new Stylesheet( source, parsed, options.filename, options.cascade !== false );
50119

51-
validate(parsed, source, stylesheet, options);
120+
validate( parsed, source, stylesheet, options );
52121

53122
const compiler = options.generate === 'ssr' ? generateSSR : generate;
54123

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

58127
export function create(source: string, _options: CompileOptions = {}) {
59128
_options.format = 'eval';
60129

61-
const compiled = compile(source, _options);
130+
const compiled = compile( source, _options );
62131

63-
if (!compiled || !compiled.code) {
132+
if ( !compiled || !compiled.code ) {
64133
return;
65134
}
66135

67136
try {
68-
return (0,eval)(compiled.code);
137+
return (0, eval)( compiled.code );
69138
} catch (err) {
70-
if (_options.onerror) {
71-
_options.onerror(err);
139+
if ( _options.onerror ) {
140+
_options.onerror( err );
72141
return;
73142
} else {
74143
throw err;

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: 169 additions & 0 deletions
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+
}

0 commit comments

Comments
 (0)