Merge pull request #1981 from sveltejs/gh-1973

Multiple preprocessors
pull/1983/head
Rich Harris 6 years ago committed by GitHub
commit a8f905f933
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,25 +1,25 @@
import { SourceMap } from 'magic-string';
import replaceAsync from '../utils/replaceAsync';
export interface PreprocessOptions {
export interface PreprocessorGroup {
markup?: (options: {
content: string,
filename: string
}) => { code: string, map?: SourceMap | string };
}) => { code: string, map?: SourceMap | string, dependencies?: string[] };
style?: Preprocessor;
script?: Preprocessor;
filename?: string
}
export type Preprocessor = (options: {
content: string,
attributes: Record<string, string | boolean>,
filename?: string
}) => { code: string, map?: SourceMap | string };
}) => { code: string, map?: SourceMap | string, dependencies?: string[] };
interface Processed {
code: string;
map?: SourceMap | string;
dependencies?: string[];
}
function parseAttributeValue(value: string) {
@ -39,28 +39,55 @@ function parseAttributes(str: string) {
export default async function preprocess(
source: string,
options: PreprocessOptions
preprocessor: PreprocessorGroup | PreprocessorGroup[],
options?: { filename?: string }
) {
if (options.markup) {
const processed: Processed = await options.markup({
const filename = (options && options.filename) || preprocessor.filename; // legacy
const dependencies = [];
const preprocessors = Array.isArray(preprocessor) ? preprocessor : [preprocessor];
const markup = preprocessors.map(p => p.markup).filter(Boolean);
const script = preprocessors.map(p => p.script).filter(Boolean);
const style = preprocessors.map(p => p.style).filter(Boolean);
for (const fn of markup) {
const processed: Processed = await fn({
content: source,
filename: options.filename,
filename
});
if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
source = processed ? processed.code : source;
}
if (options.style || options.script) {
for (const fn of script) {
source = await replaceAsync(
source,
/<script([^]*?)>([^]*?)<\/script>/gi,
async (match, attributes, content) => {
const processed: Processed = await fn({
content,
attributes: parseAttributes(attributes),
filename
});
if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
return processed ? `<script${attributes}>${processed.code}</script>` : match;
}
);
}
for (const fn of style) {
source = await replaceAsync(
source,
/<(script|style)([^]*?)>([^]*?)<\/\1>/gi,
async (match, type, attributes, content) => {
const processed: Processed =
type in options &&
(await options[type]({
content,
attributes: parseAttributes(attributes),
filename: options.filename,
}));
return processed ? `<${type}${attributes}>${processed.code}</${type}>` : match;
/<style([^]*?)>([^]*?)<\/style>/gi,
async (match, attributes, content) => {
const processed: Processed = await fn({
content,
attributes: parseAttributes(attributes),
filename
});
if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
return processed ? `<style${attributes}>${processed.code}</style>` : match;
}
);
}
@ -71,6 +98,9 @@ export default async function preprocess(
// script { code: scriptCode, map: scriptMap },
// markup { code: markupCode, map: markupMap },
code: source,
dependencies: [...new Set(dependencies)],
toString() {
return source;
}

@ -1,226 +1,27 @@
import * as fs from 'fs';
import * as assert from 'assert';
import { svelte } from '../helpers.js';
import { loadConfig, svelte } from '../helpers.js';
describe('preprocess', () => {
it('preprocesses entire component', () => {
const source = `
<h1>Hello __NAME__!</h1>
`;
fs.readdirSync('test/preprocess/samples').forEach(dir => {
if (dir[0] === '.') return;
const expected = `
<h1>Hello world!</h1>
`;
const config = loadConfig(`./preprocess/samples/${dir}/_config.js`);
return svelte.preprocess(source, {
markup: ({ content }) => {
return {
code: content.replace('__NAME__', 'world')
};
}
}).then(processed => {
assert.equal(processed.toString(), expected);
});
});
it('preprocesses style', () => {
const source = `
<div class='brand-color'>$brand</div>
<style>
.brand-color {
color: $brand;
}
</style>
`;
const expected = `
<div class='brand-color'>$brand</div>
<style>
.brand-color {
color: purple;
}
</style>
`;
return svelte.preprocess(source, {
style: ({ content }) => {
return {
code: content.replace('$brand', 'purple')
};
}
}).then(processed => {
assert.equal(processed.toString(), expected);
});
});
it('preprocesses style asynchronously', () => {
const source = `
<div class='brand-color'>$brand</div>
<style>
.brand-color {
color: $brand;
}
</style>
`;
const expected = `
<div class='brand-color'>$brand</div>
<style>
.brand-color {
color: purple;
}
</style>
`;
return svelte.preprocess(source, {
style: ({ content }) => {
return Promise.resolve({
code: content.replace('$brand', 'purple')
});
}
}).then(processed => {
assert.equal(processed.toString(), expected);
});
});
it('preprocesses script', () => {
const source = `
<script>
console.log(__THE_ANSWER__);
</script>
`;
const expected = `
<script>
console.log(42);
</script>
`;
if (config.solo && process.env.CI) {
throw new Error('Forgot to remove `solo: true` from test');
}
return svelte.preprocess(source, {
script: ({ content }) => {
return {
code: content.replace('__THE_ANSWER__', '42')
};
}
}).then(processed => {
assert.equal(processed.toString(), expected);
});
});
it('preprocesses multiple matching tags', () => {
const source = `
<script>
REPLACEME
</script>
<style>
SHOULD NOT BE REPLACED
</style>
<script>
REPLACEMETOO
</script>
`;
const expected = `
<script>
replaceme
</script>
<style>
SHOULD NOT BE REPLACED
</style>
<script>
replacemetoo
</script>
`;
return svelte.preprocess(source, {
script: ({ content }) => {
return {
code: content.toLowerCase()
};
}
}).then(processed => {
assert.equal(processed.toString(), expected);
});
});
it('parses attributes', () => {
const source = `
<style type='text/scss' data-foo="bar" bool></style>
`;
const expected = `
<style type='text/scss' data-foo="bar" bool>PROCESSED</style>
`;
return svelte.preprocess(source, {
style: ({ attributes }) => {
assert.deepEqual(attributes, {
type: 'text/scss',
'data-foo': 'bar',
bool: true
});
return { code: 'PROCESSED' };
}
}).then(processed => {
assert.equal(processed.toString(), expected);
});
});
it('provides filename to processing hooks', () => {
const source = `
<h1>Hello __MARKUP_FILENAME__!</h1>
<style>.red { color: __STYLE_FILENAME__; }</style>
<script>console.log('__SCRIPT_FILENAME__');</script>
`;
const expected = `
<h1>Hello file.html!</h1>
<style>.red { color: file.html; }</style>
<script>console.log('file.html');</script>
`;
return svelte.preprocess(source, {
filename: 'file.html',
markup: ({ content, filename }) => {
return {
code: content.replace('__MARKUP_FILENAME__', filename)
};
},
style: ({ content, filename }) => {
return {
code: content.replace('__STYLE_FILENAME__', filename)
};
},
script: ({ content, filename }) => {
return {
code: content.replace('__SCRIPT_FILENAME__', filename)
};
}
}).then(processed => {
assert.equal(processed.toString(), expected);
});
});
(config.skip ? it.skip : config.solo ? it.only : it)(dir, async () => {
const input = fs.readFileSync(`test/preprocess/samples/${dir}/input.html`, 'utf-8');
const expected = fs.readFileSync(`test/preprocess/samples/${dir}/output.html`, 'utf-8');
it('ignores null/undefined returned from preprocessor', () => {
const source = `
<script>
console.log('ignore me');
</script>
`;
const result = await svelte.preprocess(input, config.preprocess);
fs.writeFileSync(`test/preprocess/samples/${dir}/_actual.html`, result.code);
const expected = `
<script>
console.log('ignore me');
</script>
`;
assert.equal(result.code, expected);
return svelte.preprocess(source, {
script: () => null
}).then(processed => {
assert.equal(processed.toString(), expected);
assert.deepEqual(result.dependencies, config.dependencies || []);
});
});
});

@ -0,0 +1,15 @@
export default {
preprocess: {
style: ({ content }) => {
const dependencies = [];
const code = content.replace(/@import '(.+)';/g, (match, $1) => {
dependencies.push($1);
return '/* removed */';
});
return { code, dependencies };
}
},
dependencies: ['./foo.css']
};

@ -0,0 +1,3 @@
<style>
@import './foo.css';
</style>

@ -0,0 +1,3 @@
<style>
/* removed */
</style>

@ -0,0 +1,20 @@
export default {
preprocess: {
filename: 'file.html',
markup: ({ content, filename }) => {
return {
code: content.replace('__MARKUP_FILENAME__', filename)
};
},
style: ({ content, filename }) => {
return {
code: content.replace('__STYLE_FILENAME__', filename)
};
},
script: ({ content, filename }) => {
return {
code: content.replace('__SCRIPT_FILENAME__', filename)
};
}
}
};

@ -0,0 +1,3 @@
<h1>Hello __MARKUP_FILENAME__!</h1>
<style>.red { color: __STYLE_FILENAME__; }</style>
<script>console.log('__SCRIPT_FILENAME__');</script>

@ -0,0 +1,3 @@
<h1>Hello file.html!</h1>
<style>.red { color: file.html; }</style>
<script>console.log('file.html');</script>

@ -0,0 +1,5 @@
export default {
preprocess: {
script: () => null
}
};

@ -0,0 +1,3 @@
<script>
console.log('ignore me');
</script>

@ -0,0 +1,3 @@
<script>
console.log('ignore me');
</script>

@ -0,0 +1,9 @@
export default {
preprocess: {
markup: ({ content }) => {
return {
code: content.replace('__NAME__', 'world')
};
}
}
};

@ -0,0 +1 @@
<h1>Hello __NAME__!</h1>

@ -0,0 +1,14 @@
export default {
preprocess: [
{
markup: ({ content }) => ({ code: content.replace(/one/g, 'two') }),
script: ({ content }) => ({ code: content.replace(/three/g, 'four') }),
style: ({ content }) => ({ code: content.replace(/four/g, 'five') })
},
{
markup: ({ content }) => ({ code: content.replace(/two/g, 'three') }),
script: ({ content }) => ({ code: content.replace(/four/g, 'five') }),
style: ({ content }) => ({ code: content.replace(/three/g, 'four') })
}
]
};

@ -0,0 +1,11 @@
<p>one</p>
<style>
.one {
color: red;
}
</style>
<script>
console.log('one');
</script>

@ -0,0 +1,11 @@
<p>three</p>
<style>
.four {
color: red;
}
</style>
<script>
console.log('five');
</script>

@ -0,0 +1,9 @@
export default {
preprocess: {
script: ({ content }) => {
return {
code: content.toLowerCase()
};
}
}
};

@ -0,0 +1,9 @@
<script>
REPLACEME
</script>
<style>
SHOULD NOT BE REPLACED
</style>
<script>
REPLACEMETOO
</script>

@ -0,0 +1,9 @@
<script>
replaceme
</script>
<style>
SHOULD NOT BE REPLACED
</style>
<script>
replacemetoo
</script>

@ -0,0 +1,9 @@
export default {
preprocess: {
script: ({ content }) => {
return {
code: content.replace('__THE_ANSWER__', '42')
};
}
}
};

@ -0,0 +1,3 @@
<script>
console.log(__THE_ANSWER__);
</script>

@ -0,0 +1,3 @@
<script>
console.log(42);
</script>

@ -0,0 +1,9 @@
export default {
preprocess: {
style: ({ content }) => {
return Promise.resolve({
code: content.replace('$brand', 'purple')
});
}
}
};

@ -0,0 +1,7 @@
<div class='brand-color'>$brand</div>
<style>
.brand-color {
color: $brand;
}
</style>

@ -0,0 +1,7 @@
<div class='brand-color'>$brand</div>
<style>
.brand-color {
color: purple;
}
</style>

@ -0,0 +1,14 @@
import * as assert from 'assert';
export default {
preprocess: {
style: ({ attributes }) => {
assert.deepEqual(attributes, {
type: 'text/scss',
'data-foo': 'bar',
bool: true
});
return { code: 'PROCESSED' };
}
}
};

@ -0,0 +1 @@
<style type='text/scss' data-foo="bar" bool></style>

@ -0,0 +1 @@
<style type='text/scss' data-foo="bar" bool>PROCESSED</style>

@ -0,0 +1,9 @@
export default {
preprocess: {
style: ({ content }) => {
return {
code: content.replace('$brand', 'purple')
};
}
}
};

@ -0,0 +1,7 @@
<div class='brand-color'>$brand</div>
<style>
.brand-color {
color: $brand;
}
</style>

@ -0,0 +1,7 @@
<div class='brand-color'>$brand</div>
<style>
.brand-color {
color: purple;
}
</style>
Loading…
Cancel
Save