Repository: sveltejs/svelte (JavaScript)
Date: 2017-12-07 18:51:32+00:00
Task type: Feature

Issue:
svelte.preprocess has no file path awareness
I am trying to get the new `svelte.preprocess` functionality working with my [`modular-css`](https://github.com/tivac/modular-css) project, but currently none of the hooks know anything about the file they're operating on. This is problematic for anyone who wants to do things that could involve external files because it makes file resolution in any sane sort of way impossible.

I could solve this with a slightly gnarly closure variable for the case where I'm using `svelte.preprocess` directly, but I don't think that is very likely. I'm using `rollup-plugin-svelte` currently and have no desire to write my own kludgy thing instead if I can avoid it.

I have a plan that seems pretty achievable and I'd love to get your thoughts, @Rich-Harris.

I could modify `svelte.preprocess` so that each of the hooks got passed the `options` object along with the `content` to manipulate. That would make it possible to pass around more context, like the file path being manipulated. Then `rollup-plugin-svelte` would be modified to pass the `id` into the call to `svelte.preprocess()` and anyone who wants to manipulate the sections could do it with enough context to resolve `@import` statements or anything else they want.

Thoughts? I'm happy to fumble my way through a PR for this, I **really** want to get `modular-css` working with `svelte.preprocess()` so I can shrink our bundles!


Issue discussion:

Solution patch:
diff --git a/src/index.ts b/src/index.ts
--- a/src/index.ts
+++ b/src/index.ts
@@ -50,7 +50,7 @@ function parseAttributes(str: string) {
 	return attrs;
 }
 
-async function replaceTagContents(source, type: 'script' | 'style', preprocessor: Preprocessor) {
+async function replaceTagContents(source, type: 'script' | 'style', preprocessor: Preprocessor, options: PreprocessOptions) {
 	const exp = new RegExp(`<${type}([\\S\\s]*?)>([\\S\\s]*?)<\\/${type}>`, 'ig');
 	const match = exp.exec(source);
 
@@ -59,7 +59,8 @@ async function replaceTagContents(source, type: 'script' | 'style', preprocessor
 		const content: string = match[2];
 		const processed: { code: string, map?: SourceMap | string } = await preprocessor({
 			content,
-			attributes
+			attributes,
+			filename : options.filename
 		});
 
 		if (processed && processed.code) {
@@ -77,16 +78,19 @@ async function replaceTagContents(source, type: 'script' | 'style', preprocessor
 export async function preprocess(source: string, options: PreprocessOptions) {
 	const { markup, style, script } = options;
 	if (!!markup) {
-		const processed: { code: string, map?: SourceMap | string } = await markup({ content: source });
+		const processed: { code: string, map?: SourceMap | string } = await markup({
+			content: source,
+			filename: options.filename
+		});
 		source = processed.code;
 	}
 
 	if (!!style) {
-		source = await replaceTagContents(source, 'style', style);
+		source = await replaceTagContents(source, 'style', style, options);
 	}
 
 	if (!!script) {
-		source = await replaceTagContents(source, 'script', script);
+		source = await replaceTagContents(source, 'script', script, options);
 	}
 
 	return {
diff --git a/src/interfaces.ts b/src/interfaces.ts
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -83,9 +83,10 @@ export interface CustomElementOptions {
 }
 
 export interface PreprocessOptions {
-	markup?: (options: {content: string}) => { code: string, map?: SourceMap | string };
+	markup?: (options: {content: string, filename: string}) => { code: string, map?: SourceMap | string };
 	style?: Preprocessor;
 	script?: Preprocessor;
+	filename?: string
 }
 
-export type Preprocessor = (options: {content: string, attributes: Record<string, string | boolean>}) => { code: string, map?: SourceMap | string };
+export type Preprocessor = (options: {content: string, attributes: Record<string, string | boolean>, filename?: string}) => { code: string, map?: SourceMap | string };


Test patch:
diff --git a/test/preprocess/index.js b/test/preprocess/index.js
--- a/test/preprocess/index.js
+++ b/test/preprocess/index.js
@@ -126,6 +126,41 @@ describe('preprocess', () => {
 		});
 	});
 
+	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);
+		});
+	});
+
 	it('ignores null/undefined returned from preprocessor', () => {
 		const source = `
 			<script>
@@ -145,4 +180,4 @@ describe('preprocess', () => {
 			assert.equal(processed.toString(), expected);
 		});
 	});
-});
\ No newline at end of file
+});