Repository: sveltejs/svelte (JavaScript)
Date: 2021-07-26 04:09:48+00:00
Task type: Bug Fix

Issue:
`export from` generates wrong code
```html
<script>
export { a } from 'b';
</script>
```

That should yield the same result as this:
```html
<script>
import { a } from 'b';
export { a };
</script>
```

But it doesn’t include the necessary `import`.


Issue discussion:
There are also some issues with the same code in a module script:

```html
<script context='module'>
	export { a } from 'b';
</script>
```

In this case, the export could probably pass through unchanged in the compiled component. It is instead for some reason defining `a` as a read-only prop on the _instance_, referring to a global `a` which is not imported.
I was just taking a look at this for real, and it is rather uglier than I was hoping. Some random thoughts:

In `component.extract_imports` we _probably_ want to also extract out `ExportNamedDeclaration` nodes with a `source` present. Then in `wrapModule` (which is passed `component.imports` we can account for both types of nodes and generate the appropriate imports.

In `component.extract_exports` we also need to handle the case where we have an `ExportNamedDeclaration` with a `source`. Perhaps this can be handled in the same 'no `declaration`' case, but then we'd need to get each of the `export ... from ...` variables into `component.var_lookup` in some way. (There's also a TODO in there `// TODO what happens with `export { Math }` or some other global?` which seems to be an unpleasant case.) An alternative would be to have an entirely separate case before the `else` where we handle the presence of a `source`.
I haven't looked at this specific issue yet but just wanted to flag up something I learned the hard way on Rollup — `export { a } from './x.js'` *doesn't* create a local binding to `a`:

```js
export { a } from './x.js';

console.log(a); // ReferenceError: a is not defined
```

That may affect the code generation here, I'm not sure
The `export { Math };` thing is fixed in Acorn 6.0.7, so we won't have to worry about that if we upgrade, as the parser will complain right away about that. I am thinking though that for now we should just throw a compiler error if there are any `export from`s and we can deal with properly handling that at another time.
Beta 16 refuses to compile code with `export ... from`s. Gonna abuse issues slightly and switch this one from a bug to an enhancement, and use it later for adding support for this.
New destructured assignment export syntax is not working :
```js
export let {a: b} = "hello";
```
Should result in something like: 
```js
let a = "hello";
export {a as b};
```
But it is throwing an error.
Repl: https://svelte.dev/repl/3675adb0b0d143d7864abcdc20ad7d88?version=3.24.0
Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

No, no, no, no, no, *please* turn the @stale bot off. It’s a menace that does almost nothing good and causes *significant* harm. Everyone hates it.
We are drowning, i don't doubt you dislike it but if we want any hope of prioritising important issues we need to automate some stuff. As you can see, a simple comment dismisses it.
> It’s a menace that does almost nothing good and causes significant harm.

Actually, it brought this ticket back into a semi-conscious state. I'd call that good.
I have never seen the @stale bot or any directly equivalent to it achieve a net positive outcome. *Never.* It results in disgruntled people, extra expenditure of effort (for reporters *and maintainers*), real stuff getting lost when people get fed up with poking the bot (I have no intention of poking it further), and more extensive filing of duplicates.

You say a simple comment dismisses it, but it *doesn’t*—it only does *this time*. Next time, it continues to annoy.

This is an *issue tracker*. Use labels, projects, milestones and the likes for prioritising stuff. Not sweeping stuff under the carpet.
It is an issue tracker but we don't have a backlog, or planning sessions, or a project board. Or the resources to even triage and tag effectively. 

If it is important someone will respond / reopen, popular issues are exempt from the bot, we can't fix everything and this is pretty much our only view on stuff that need to be addressed. We need to make some attempt to make sure that everything is still relevant and reduce the noise to a degree where we can actually manage it.

I understand the trade-offs with stale bots but we don't have many options. I appreciate your experiences but that doesn't make them a fact. We have discussed this internally and this is what we are doing. If you have any other actionable alternatives outside of saying the bot is bad then we are all ears.
Closing issues doesn’t solve anything. Closing issues in GitHub just sweeps them under the carpet and helps everyone to forget about them, which is just not what you want—the fact that GitHub search excludes closed items by default is a large part of the problem with it.

As applied to software projects with general-purpose issue trackers, the @stale bot is fundamentally *phenomenally* bad idea, a road paved with good intentions.

I presented an actionable alternative: labels. *Possibly* automatically applied, but it’s certainly better to spend a little bit of time on manual triage. It honestly doesn’t take long to skim through a few hundred issues and bin them into labels.

609 open issues? That’s honestly not a problem. Not at all. There’s nothing wrong with having a large number of issues open, if they do correspond to real things—even things that you may not expect to get to for years, if ever, because that might change or someone might decide they want to deal with one.

Closing issues that aren’t dealt with is bad. Please don’t do it.
You make valid points but we'll need to figure out a pragmatic way to do it. We will think on it. Most issues have been manually labelled as stale rather than automated and closure will be manual too, so we have time to think.

This is the wrong place for this conversation though.

Solution patch:
diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts
--- a/src/compiler/compile/Component.ts
+++ b/src/compiler/compile/Component.ts
@@ -24,7 +24,7 @@ import TemplateScope from './nodes/shared/TemplateScope';
 import fuzzymatch from '../utils/fuzzymatch';
 import get_object from './utils/get_object';
 import Slot from './nodes/Slot';
-import { Node, ImportDeclaration, Identifier, Program, ExpressionStatement, AssignmentExpression, Literal } from 'estree';
+import { Node, ImportDeclaration, ExportNamedDeclaration, Identifier, Program, ExpressionStatement, AssignmentExpression, Literal, ExportDefaultDeclaration, ExportAllDeclaration } from 'estree';
 import add_to_set from './utils/add_to_set';
 import check_graph_for_cycles from './utils/check_graph_for_cycles';
 import { print, x, b } from 'code-red';
@@ -70,6 +70,8 @@ export default class Component {
 	var_lookup: Map<string, Var> = new Map();
 
 	imports: ImportDeclaration[] = [];
+	exports_from: ExportNamedDeclaration[] = [];
+	instance_exports_from: ExportNamedDeclaration[] = [];
 
 	hoistable_nodes: Set<Node> = new Set();
 	node_for_declaration: Map<string, Node> = new Map();
@@ -333,7 +335,8 @@ export default class Component {
 					.map(variable => ({
 						name: variable.name,
 						as: variable.export_name
-					}))
+					})),
+				this.exports_from
 			);
 
 			css = compile_options.customElement
@@ -492,22 +495,27 @@ export default class Component {
 		this.imports.push(node);
 	}
 
-	extract_exports(node) {
+	extract_exports(node, module_script = false) {
 		const ignores = extract_svelte_ignore_from_comments(node);
 		if (ignores.length) this.push_ignores(ignores);
-		const result = this._extract_exports(node);
+		const result = this._extract_exports(node, module_script);
 		if (ignores.length) this.pop_ignores();
 		return result;
 	}
 
-	private _extract_exports(node) {
+	private _extract_exports(node: ExportDefaultDeclaration | ExportNamedDeclaration | ExportAllDeclaration, module_script) {
 		if (node.type === 'ExportDefaultDeclaration') {
-			return this.error(node, compiler_errors.default_export);
+			return this.error(node as any, compiler_errors.default_export);
 		}
 
 		if (node.type === 'ExportNamedDeclaration') {
 			if (node.source) {
-				return this.error(node, compiler_errors.not_implemented);
+				if (module_script) {
+					this.exports_from.push(node);
+				} else {
+					this.instance_exports_from.push(node);
+				}
+				return null;
 			}
 			if (node.declaration) {
 				if (node.declaration.type === 'VariableDeclaration') {
@@ -516,7 +524,7 @@ export default class Component {
 							const variable = this.var_lookup.get(name);
 							variable.export_name = name;
 							if (variable.writable && !(variable.referenced || variable.referenced_from_script || variable.subscribable)) {
-								this.warn(declarator, compiler_warnings.unused_export_let(this.name.name, name));
+								this.warn(declarator as any, compiler_warnings.unused_export_let(this.name.name, name));
 							}
 						});
 					});
@@ -536,7 +544,7 @@ export default class Component {
 						variable.export_name = specifier.exported.name;
 
 						if (variable.writable && !(variable.referenced || variable.referenced_from_script || variable.subscribable)) {
-							this.warn(specifier, compiler_warnings.unused_export_let(this.name.name, specifier.exported.name));
+							this.warn(specifier as any, compiler_warnings.unused_export_let(this.name.name, specifier.exported.name));
 						}
 					}
 				});
@@ -612,7 +620,7 @@ export default class Component {
 			}
 
 			if (/^Export/.test(node.type)) {
-				const replacement = this.extract_exports(node);
+				const replacement = this.extract_exports(node, true);
 				if (replacement) {
 					body[i] = replacement;
 				} else {
diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts
--- a/src/compiler/compile/compiler_errors.ts
+++ b/src/compiler/compile/compiler_errors.ts
@@ -174,10 +174,6 @@ export default {
 		code: 'default-export',
 		message: 'A component cannot have a default export'
 	},
-	not_implemented: {
-		code: 'not-implemented',
-		message: 'A component currently cannot have an export ... from'
-	},
 	illegal_declaration: {
 		code: 'illegal-declaration',
 		message: 'The $ prefix is reserved, and cannot be used for variable and import names'
diff --git a/src/compiler/compile/create_module.ts b/src/compiler/compile/create_module.ts
--- a/src/compiler/compile/create_module.ts
+++ b/src/compiler/compile/create_module.ts
@@ -1,7 +1,7 @@
 import list from '../utils/list';
 import { ModuleFormat } from '../interfaces';
 import { b, x } from 'code-red';
-import { Identifier, ImportDeclaration } from 'estree';
+import { Identifier, ImportDeclaration, ExportNamedDeclaration } from 'estree';
 
 const wrappers = { esm, cjs };
 
@@ -19,20 +19,21 @@ export default function create_module(
 	helpers: Array<{ name: string; alias: Identifier }>,
 	globals: Array<{ name: string; alias: Identifier }>,
 	imports: ImportDeclaration[],
-	module_exports: Export[]
+	module_exports: Export[],
+	exports_from: ExportNamedDeclaration[]
 ) {
 	const internal_path = `${sveltePath}/internal`;
 
 	helpers.sort((a, b) => (a.name < b.name) ? -1 : 1);
 	globals.sort((a, b) => (a.name < b.name) ? -1 : 1);
+	
+	const formatter = wrappers[format];
 
-	if (format === 'esm') {
-		return esm(program, name, banner, sveltePath, internal_path, helpers, globals, imports, module_exports);
+	if (!formatter) {
+		throw new Error(`options.format is invalid (must be ${list(Object.keys(wrappers))})`);
 	}
 
-	if (format === 'cjs') return cjs(program, name, banner, sveltePath, internal_path, helpers, globals, imports, module_exports);
-
-	throw new Error(`options.format is invalid (must be ${list(Object.keys(wrappers))})`);
+	return formatter(program, name, banner, sveltePath, internal_path, helpers, globals, imports, module_exports, exports_from);
 }
 
 function edit_source(source, sveltePath) {
@@ -76,7 +77,8 @@ function esm(
 	helpers: Array<{ name: string; alias: Identifier }>,
 	globals: Array<{ name: string; alias: Identifier }>,
 	imports: ImportDeclaration[],
-	module_exports: Export[]
+	module_exports: Export[],
+	exports_from: ExportNamedDeclaration[]
 ) {
 	const import_declaration = {
 		type: 'ImportDeclaration',
@@ -94,6 +96,9 @@ function esm(
 	imports.forEach(node => {
 		node.source.value = edit_source(node.source.value, sveltePath);
 	});
+	exports_from.forEach(node => {
+		node.source!.value = edit_source(node.source!.value, sveltePath);
+	});
 
 	const exports = module_exports.length > 0 && {
 		type: 'ExportNamedDeclaration',
@@ -110,6 +115,7 @@ function esm(
 		${import_declaration}
 		${internal_globals}
 		${imports}
+		${exports_from}
 
 		${program.body}
 
@@ -127,7 +133,8 @@ function cjs(
 	helpers: Array<{ name: string; alias: Identifier }>,
 	globals: Array<{ name: string; alias: Identifier }>,
 	imports: ImportDeclaration[],
-	module_exports: Export[]
+	module_exports: Export[],
+	exports_from: ExportNamedDeclaration[]
 ) {
 	const internal_requires = {
 		type: 'VariableDeclaration',
@@ -183,6 +190,13 @@ function cjs(
 
 	const exports = module_exports.map(x => b`exports.${{ type: 'Identifier', name: x.as }} = ${{ type: 'Identifier', name: x.name }};`);
 
+	const user_exports_from = exports_from.map(node => {
+		const init = x`require("${edit_source(node.source.value, sveltePath)}")`;
+		return node.specifiers.map(specifier => {
+			return b`exports.${specifier.exported} = ${init}.${specifier.local};`;
+		});
+	});
+
 	program.body = b`
 		/* ${banner} */
 
@@ -190,6 +204,7 @@ function cjs(
 		${internal_requires}
 		${internal_globals}
 		${user_requires}
+		${user_exports_from}
 
 		${program.body}
 
diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts
--- a/src/compiler/compile/render_dom/index.ts
+++ b/src/compiler/compile/render_dom/index.ts
@@ -6,7 +6,7 @@ import { walk } from 'estree-walker';
 import { extract_names, Scope } from 'periscopic';
 import { invalidate } from './invalidate';
 import Block from './Block';
-import { ClassDeclaration, FunctionExpression, Node, Statement, ObjectExpression, Expression } from 'estree';
+import { ImportDeclaration, ClassDeclaration, FunctionExpression, Node, Statement, ObjectExpression, Expression } from 'estree';
 import { apply_preprocessor_sourcemap } from '../../utils/mapped_code';
 import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types';
 import { flatten } from '../../utils/flatten';
@@ -174,6 +174,46 @@ export default function dom(
 		}
 	});
 
+	component.instance_exports_from.forEach(exports_from => {
+		const import_declaration = {
+			...exports_from,
+			type: 'ImportDeclaration',
+			specifiers: [],
+			source: exports_from.source
+		};
+		component.imports.push(import_declaration as ImportDeclaration);
+
+		exports_from.specifiers.forEach(specifier => {
+			if (component.component_options.accessors) {
+				const name = component.get_unique_name(specifier.exported.name);
+				import_declaration.specifiers.push({
+					...specifier,
+					type: 'ImportSpecifier',
+					imported: specifier.local,
+					local: name
+				});
+
+				accessors.push({
+					type: 'MethodDefinition',
+					kind: 'get',
+					key: { type: 'Identifier', name: specifier.exported.name },
+					value: x`function() {
+						return ${name}
+					}`
+				});
+			} else if (component.compile_options.dev) {
+				accessors.push({
+					type: 'MethodDefinition',
+					kind: 'get',
+					key: { type: 'Identifier', name: specifier.exported.name },
+					value: x`function() {
+						throw new @_Error("<${component.tag}>: Props cannot be read directly from the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");
+					}`
+				});
+			}
+		});
+	});
+
 	if (component.compile_options.dev) {
 		// checking that expected ones were passed
 		const expected = props.filter(prop => prop.writable && !prop.initialised);


Test patch:
diff --git a/test/js/samples/export-from-accessors/_config.js b/test/js/samples/export-from-accessors/_config.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/export-from-accessors/_config.js
@@ -0,0 +1,5 @@
+export default {
+	options: {
+		accessors: true
+	}
+};
diff --git a/test/js/samples/export-from-accessors/expected.js b/test/js/samples/export-from-accessors/expected.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/export-from-accessors/expected.js
@@ -0,0 +1,34 @@
+/* generated by Svelte vX.Y.Z */
+import { SvelteComponent, init, safe_not_equal } from "svelte/internal";
+
+import { f as f_1, g as g_1 } from './d';
+import { h as h_1 } from './e';
+import { i as j } from './f';
+export { d as e } from './c';
+export { c } from './b';
+export { a, b } from './a';
+
+class Component extends SvelteComponent {
+	constructor(options) {
+		super();
+		init(this, options, null, null, safe_not_equal, {});
+	}
+
+	get f() {
+		return f_1;
+	}
+
+	get g() {
+		return g_1;
+	}
+
+	get h() {
+		return h_1;
+	}
+
+	get j() {
+		return j;
+	}
+}
+
+export default Component;
\ No newline at end of file
diff --git a/test/js/samples/export-from-accessors/input.svelte b/test/js/samples/export-from-accessors/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/js/samples/export-from-accessors/input.svelte
@@ -0,0 +1,11 @@
+<script context="module">
+  export { a, b } from './a';
+  export { c } from './b';
+  export { d as e } from './c';
+</script>
+
+<script>
+  export { f, g } from './d';
+  export { h } from './e';
+  export { i as j } from './f';
+</script>
diff --git a/test/js/samples/export-from-cjs/_config.js b/test/js/samples/export-from-cjs/_config.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/export-from-cjs/_config.js
@@ -0,0 +1,6 @@
+export default {
+	options: {
+		accessors: true,
+		format: 'cjs'
+	}
+};
diff --git a/test/js/samples/export-from-cjs/expected.js b/test/js/samples/export-from-cjs/expected.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/export-from-cjs/expected.js
@@ -0,0 +1,36 @@
+/* generated by Svelte vX.Y.Z */
+"use strict";
+
+const { SvelteComponent, init, safe_not_equal } = require("svelte/internal");
+const { f: f_1, g: g_1 } = require("./d");
+const { h: h_1 } = require("./e");
+const { i: j } = require("./f");
+exports.e = require("./c").d;
+exports.c = require("./b").c;
+exports.a = require("./a").a;
+exports.b = require("./a").b;
+
+class Component extends SvelteComponent {
+	constructor(options) {
+		super();
+		init(this, options, null, null, safe_not_equal, {});
+	}
+
+	get f() {
+		return f_1;
+	}
+
+	get g() {
+		return g_1;
+	}
+
+	get h() {
+		return h_1;
+	}
+
+	get j() {
+		return j;
+	}
+}
+
+exports.default = Component;
\ No newline at end of file
diff --git a/test/js/samples/export-from-cjs/input.svelte b/test/js/samples/export-from-cjs/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/js/samples/export-from-cjs/input.svelte
@@ -0,0 +1,11 @@
+<script context="module">
+  export { a, b } from './a';
+  export { c } from './b';
+  export { d as e } from './c';
+</script>
+
+<script>
+  export { f, g } from './d';
+  export { h } from './e';
+  export { i as j } from './f';
+</script>
diff --git a/test/js/samples/export-from/expected.js b/test/js/samples/export-from/expected.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/export-from/expected.js
@@ -0,0 +1,18 @@
+/* generated by Svelte vX.Y.Z */
+import { SvelteComponent, init, safe_not_equal } from "svelte/internal";
+
+import './d';
+import './e';
+import './f';
+export { d as e } from './c';
+export { c } from './b';
+export { a, b } from './a';
+
+class Component extends SvelteComponent {
+	constructor(options) {
+		super();
+		init(this, options, null, null, safe_not_equal, {});
+	}
+}
+
+export default Component;
\ No newline at end of file
diff --git a/test/js/samples/export-from/input.svelte b/test/js/samples/export-from/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/js/samples/export-from/input.svelte
@@ -0,0 +1,11 @@
+<script context="module">
+  export { a, b } from './a';
+  export { c } from './b';
+  export { d as e } from './c';
+</script>
+
+<script>
+  export { f, g } from './d';
+  export { h } from './e';
+  export { i as j } from './f';
+</script>
diff --git a/test/runtime/samples/export-from/A.svelte b/test/runtime/samples/export-from/A.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/export-from/A.svelte
@@ -0,0 +1,23 @@
+<script context="module">
+  export { a, b } from './B.svelte';
+  export { c as d } from './B.svelte';
+</script>
+
+<script>
+  export { d, e } from './B.svelte';
+  export { f as g } from './B.svelte';
+  let e = 123;
+  let b = 234;
+  function foo() {
+    e = 456;
+    b = 567;
+  }
+</script>
+
+a: {typeof a}<br />
+b: {typeof b}<br />
+c: {typeof c}<br />
+d: {typeof d}<br />
+e: {typeof e}<br />
+f: {typeof f}<br />
+g: {typeof g}<br />
\ No newline at end of file
diff --git a/test/runtime/samples/export-from/B.svelte b/test/runtime/samples/export-from/B.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/export-from/B.svelte
@@ -0,0 +1,8 @@
+<script context="module">
+  export const a = 'a';
+  export const b = 'b';
+  export const c = 'c';
+  export const d = 'd';
+  export const e = 'e';
+  export const f = 'f';
+</script>
diff --git a/test/runtime/samples/export-from/_config.js b/test/runtime/samples/export-from/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/export-from/_config.js
@@ -0,0 +1,28 @@
+export default {
+	html: `
+		a,b,undefined,c
+		<br />
+		a: undefined<br />
+		b: number<br />
+		c: undefined<br />
+		d: undefined<br />
+		e: number<br />
+		f: undefined<br />
+		g: undefined<br />
+		<br />
+		{"d":"d","e":"e","g":"f"}
+	`,
+	ssrHtml: `
+		a,b,undefined,c
+		<br />
+		a: undefined<br />
+		b: number<br />
+		c: undefined<br />
+		d: undefined<br />
+		e: number<br />
+		f: undefined<br />
+		g: undefined<br />
+		<br />
+		{}
+	`
+};
diff --git a/test/runtime/samples/export-from/main.svelte b/test/runtime/samples/export-from/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/export-from/main.svelte
@@ -0,0 +1,21 @@
+<script>
+	import A, { a, b, c, d } from './A.svelte';
+	import {onMount} from 'svelte';
+
+	let component;
+	let props = {};
+	onMount(() => {
+		props = {
+			d: component.d,
+			e: component.e,
+			f: component.f,
+			g: component.g,
+		};
+	});
+</script>
+
+{a},{b},{c},{d}
+<br />
+<A bind:this={component} />
+<br />
+{JSON.stringify(props)}
\ No newline at end of file