Skip to content

Commit

Permalink
Basic support for style and script preprocessors
Browse files Browse the repository at this point in the history
Suggestion for sveltejs#181 and sveltejs#876
  • Loading branch information
esarbanis committed Nov 29, 2017
1 parent 72bd23a commit 953c296
Show file tree
Hide file tree
Showing 25 changed files with 970 additions and 47 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"acorn": "^5.1.1",
"chalk": "^2.0.1",
"codecov": "^2.2.0",
"coffeescript": "^2.0.2",
"console-group": "^0.3.2",
"css-tree": "1.0.0-alpha22",
"eslint": "^4.3.0",
Expand All @@ -55,13 +56,16 @@
"estree-walker": "^0.5.1",
"glob": "^7.1.1",
"jsdom": "^11.1.0",
"less": "^2.7.3",
"locate-character": "^2.0.0",
"magic-string": "^0.22.3",
"mocha": "^3.2.0",
"nightmare": "^2.10.0",
"node-resolve": "^1.3.3",
"node-sass": "^4.7.1",
"nyc": "^11.1.0",
"prettier": "^1.7.0",
"pug": "^2.0.0-rc.4",
"reify": "^0.12.3",
"rollup": "^0.48.2",
"rollup-plugin-buble": "^0.15.0",
Expand All @@ -74,6 +78,7 @@
"rollup-watch": "^4.3.1",
"source-map": "^0.5.6",
"source-map-support": "^0.4.8",
"stylus": "^0.54.5",
"ts-node": "^3.3.0",
"tslib": "^1.8.0",
"typescript": "^2.6.1"
Expand Down
80 changes: 75 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import generate from './generators/dom/index';
import generateSSR from './generators/server-side-rendering/index';
import { assign } from './shared/index.js';
import Stylesheet from './css/Stylesheet';
import { Parsed, CompileOptions, Warning } from './interfaces';
import { Parsed, CompileOptions, Warning, PreprocessOptions, Preprocessor } from './interfaces';
import { SourceMap } from 'magic-string';

const version = '__VERSION__';

function normalizeOptions(options: CompileOptions): CompileOptions {
let normalizedOptions = assign({ generate: 'dom' }, options);
let normalizedOptions = assign({ generate: 'dom', preprocessor: false }, options);
const { onwarn, onerror } = normalizedOptions;
normalizedOptions.onwarn = onwarn
? (warning: Warning) => onwarn(warning, defaultOnwarn)
Expand All @@ -34,9 +35,78 @@ function defaultOnerror(error: Error) {
throw error;
}

function _parseAttributeValue(value: string | boolean) {
const curated = (<string>value).replace(/"/ig, '');
if (curated === 'true' || curated === 'false') {
return curated === 'true';
}
return curated;
}

function _parseStyleAttributes(str: string) {
const attrs = {};
str.split(/\s+/).filter(Boolean).forEach(attr => {
const [name, value] = attr.split('=');
attrs[name] = _parseAttributeValue(value);
});
return attrs;
}

async function _doPreprocess(source, type: 'script' | 'style', preprocessor: Preprocessor) {
const exp = new RegExp(`<${type}([\\S\\s]*?)>([\\S\\s]*?)<\\/${type}>`, 'ig');
const match = exp.exec(source);
if (match) {
const attributes: Record<string, string | boolean> = _parseStyleAttributes(match[1]);
const content: string = match[2];
const processed: { code: string, map?: SourceMap | string } = await preprocessor({
content,
attributes
});
return source.replace(content, processed.code || content);
}
}

export async function preprocess(source: string, options: PreprocessOptions) {
const { markup, style, script } = options;
if (!!markup) {
try {
const processed: { code: string, map?: SourceMap | string } = await markup({ content: source });
source = processed.code;
} catch (error) {
defaultOnerror(error);
}
}

if (!!style) {
try {
source = await _doPreprocess(source, 'style', style);
} catch (error) {
defaultOnerror(error);
}
}

if (!!script) {
try {
source = await _doPreprocess(source, 'script', script);
} catch (error) {
defaultOnerror(error);
}
}

return {
// TODO return separated output, in future version where svelte.compile supports it:
// style: { code: styleCode, map: styleMap },
// script { code: scriptCode, map: scriptMap },
// markup { code: markupCode, map: markupMap },

toString() {
return source;
}
};
}

export function compile(source: string, _options: CompileOptions) {
const options = normalizeOptions(_options);

let parsed: Parsed;

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

return compiler(parsed, source, stylesheet, options);
}
};

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

try {
return (0,eval)(compiled.code);
return (0, eval)(compiled.code);
} catch (err) {
if (_options.onerror) {
_options.onerror(err);
Expand Down
13 changes: 12 additions & 1 deletion src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {SourceMap} from 'magic-string';

export interface Node {
start: number;
end: number;
Expand Down Expand Up @@ -60,6 +62,7 @@ export interface CompileOptions {

onerror?: (error: Error) => void;
onwarn?: (warning: Warning) => void;
preprocessor?: ((raw: string) => string) | false ;
}

export interface GenerateOptions {
Expand All @@ -78,4 +81,12 @@ export interface Visitor {
export interface CustomElementOptions {
tag?: string;
props?: string[];
}
}

export interface PreprocessOptions {
markup?: (options: {content: string}) => { code: string, map?: SourceMap | string };
style?: Preprocessor;
script?: Preprocessor;
}

export type Preprocessor = (options: {content: string, attributes: Record<string, string | boolean>}) => { code: string, map?: SourceMap | string };
169 changes: 169 additions & 0 deletions test/preprocess/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import assert from 'assert';
import * as fs from 'fs';
import {parse} from 'acorn';
import {addLineNumbers, env, normalizeHtml, svelte} from '../helpers.js';

function tryRequire(file) {
try {
const mod = require(file);
return mod.default || mod;
} catch (err) {
if (err.code !== 'MODULE_NOT_FOUND') throw err;
return null;
}
}

function normalizeWarning(warning) {
warning.frame = warning.frame.replace(/^\n/, '').
replace(/^\t+/gm, '').
replace(/\s+$/gm, '');
delete warning.filename;
delete warning.toString;
return warning;
}

function checkCodeIsValid(code) {
try {
parse(code);
} catch (err) {
console.error(addLineNumbers(code));
throw new Error(err.message);
}
}

describe('preprocess', () => {
fs.readdirSync('test/preprocess/samples').forEach(dir => {
if (dir[0] === '.') return;

// add .solo to a sample directory name to only run that test
const solo = /\.solo/.test(dir);
const skip = /\.skip/.test(dir);

if (solo && process.env.CI) {
throw new Error('Forgot to remove `solo: true` from test');
}

(solo ? it.only : skip ? it.skip : it)(dir, () => {
const config = tryRequire(`./samples/${dir}/_config.js`) || {};
const input = fs.existsSync(`test/preprocess/samples/${dir}/input.pug`) ?
fs.readFileSync(`test/preprocess/samples/${dir}/input.pug`,
'utf-8').replace(/\s+$/, '') :
fs.readFileSync(`test/preprocess/samples/${dir}/input.html`,
'utf-8').replace(/\s+$/, '');

svelte.preprocess(input, config).
then(processed => processed.toString()).
then(processed => {

const expectedWarnings = (config.warnings || []).map(
normalizeWarning);
const domWarnings = [];
const ssrWarnings = [];

const dom = svelte.compile(
processed,
Object.assign(config, {
format: 'iife',
name: 'SvelteComponent',
onwarn: warning => {
domWarnings.push(warning);
},
})
);

const ssr = svelte.compile(
processed,
Object.assign(config, {
format: 'iife',
generate: 'ssr',
name: 'SvelteComponent',
onwarn: warning => {
ssrWarnings.push(warning);
},
})
);

// check the code is valid
checkCodeIsValid(dom.code);
checkCodeIsValid(ssr.code);

assert.equal(dom.css, ssr.css);

assert.deepEqual(
domWarnings.map(normalizeWarning),
ssrWarnings.map(normalizeWarning)
);
assert.deepEqual(domWarnings.map(normalizeWarning), expectedWarnings);

const expected = {
html: read(`test/preprocess/samples/${dir}/expected.html`),
css: read(`test/preprocess/samples/${dir}/expected.css`),
};

if (expected.css !== null) {
fs.writeFileSync(`test/preprocess/samples/${dir}/_actual.css`,
dom.css);
assert.equal(dom.css.replace(/svelte-\d+/g, 'svelte-xyz'),
expected.css);
}

// verify that the right elements have scoping selectors
if (expected.html !== null) {
const window = env();

// dom
try {
const Component = eval(
`(function () { ${dom.code}; return SvelteComponent; }())`
);
const target = window.document.querySelector('main');

new Component({target, data: config.data});
const html = target.innerHTML;

fs.writeFileSync(`test/preprocess/samples/${dir}/_actual.html`,
html);

assert.equal(
normalizeHtml(window,
html.replace(/svelte-\d+/g, 'svelte-xyz')),
normalizeHtml(window, expected.html)
);
} catch (err) {
console.log(dom.code);
throw err;
}

// ssr
try {
const component = eval(
`(function () { ${ssr.code}; return SvelteComponent; }())`
);

assert.equal(
normalizeHtml(
window,
component.render(config.data).
replace(/svelte-\d+/g, 'svelte-xyz')
),
normalizeHtml(window, expected.html)
);
} catch (err) {
console.log(ssr.code);
throw err;
}
}
}).catch(error => {
throw error;
});
});
});
});

function read(file) {
try {
return fs.readFileSync(file, 'utf-8');
} catch (err) {
return null;
}
}
19 changes: 19 additions & 0 deletions test/preprocess/samples/use-coffeescript-preprocessor/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as CoffeeScript from 'coffeescript';

export default {
cascade: false,
script: ({content, attributes}) => {
if (attributes.type !== 'text/coffeescript') {
return {code: content};
}

return new Promise((fulfil, reject) => {
try {
const code = CoffeeScript.compile(content, {});
fulfil({code});
} catch (error) {
reject(error);
}
});
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Hello foo!</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<h1>Hello {{name}}!</h1>

<script type="text/coffeescript">
export default {
data: () ->
name: 'foo'
};
</script>
15 changes: 15 additions & 0 deletions test/preprocess/samples/use-pug-preprocessor/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as pug from 'pug';

export default {
cascade: false,
markup: ({content}) => {
return new Promise((fulfil, reject) => {
try {
const code = pug.render(content);
fulfil({code});
} catch (error) {
reject(error);
}
});
},
};
1 change: 1 addition & 0 deletions test/preprocess/samples/use-pug-preprocessor/expected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[foo][svelte-xyz]{color:red}[baz][svelte-xyz]{color:blue}
Loading

0 comments on commit 953c296

Please sign in to comment.