diff --git a/src/compile/Component.ts b/src/compile/Component.ts index ab7c58d10f..b3aab8446a 100644 --- a/src/compile/Component.ts +++ b/src/compile/Component.ts @@ -32,6 +32,17 @@ childKeys.EachBlock = childKeys.IfBlock = ['children', 'else']; childKeys.Attribute = ['value']; childKeys.ExportNamedDeclaration = ['declaration', 'specifiers']; +function get_context(script) { + const context = script.attributes.find(attribute => attribute.name === 'context'); + if (!context) return 'default'; + + if (context.value.length !== 1 || context.value[0].type !== 'Text') { + throw new Error(`context attribute must be static`); + } + + return context.value[0].data; +} + export default class Component { stats: Stats; @@ -52,12 +63,14 @@ export default class Component { imports: Node[] = []; namespace: string; hasComponents: boolean; + module_javascript: string; javascript: string; declarations: string[] = []; writable_declarations: Set = new Set(); initialised_declarations: Set = new Set(); exports: Array<{ name: string, as: string }> = []; + module_exports: Array<{ name: string, as: string }> = []; partly_hoisted: string[] = []; fully_hoisted: string[] = []; @@ -109,7 +122,8 @@ export default class Component { this.properties = new Map(); - this.walkJs(); + this.walk_module_js(); + this.walk_instance_js(); this.name = this.alias(name); this.meta = process_meta(this, this.ast.html.children); @@ -197,7 +211,18 @@ export default class Component { ? options.shared : 'svelte/internal.js'; - const module = wrapModule(result, format, name, options, banner, sharedPath, importedHelpers, this.imports, this.source); + const module = wrapModule( + result, + format, + name, + options, + banner, + sharedPath, + importedHelpers, + this.imports, + this.module_exports, + this.source + ); const parts = module.split('✂]'); const finalChunk = parts.pop(); @@ -351,38 +376,9 @@ export default class Component { return null; } - walkJs() { - const { js } = this.ast; - if (!js) return; - - this.addSourcemapLocations(js.content); - - const { code, source, imports } = this; - - const indent = code.getIndentString(); - code.indent(indent, { - exclude: [ - [0, js.content.start], - [js.content.end, source.length] - ] - }); - - let { scope, map, globals } = createScopes(js.content); - this.scope = scope; - - scope.declarations.forEach(name => { - this.userVars.add(name); - this.declarations.push(name); - }); - - this.writable_declarations = scope.writable_declarations; - this.initialised_declarations = scope.initialised_declarations; - - globals.forEach(name => { - this.userVars.add(name); - }); - - const body = js.content.body.slice(); // slice, because we're going to be mutating the original + extract_imports_and_exports(content, imports, exports) { + const { code } = this; + const body = content.body.slice(); // TODO do we need to mutate the original? body.forEach(node => { if (node.type === 'ExportDefaultDeclaration') { @@ -397,18 +393,18 @@ export default class Component { if (node.declaration.type === 'VariableDeclaration') { node.declaration.declarations.forEach(declarator => { extractNames(declarator.id).forEach(name => { - this.exports.push({ name, as: name }); + exports.push({ name, as: name }); }); }); } else { const { name } = node.declaration.id; - this.exports.push({ name, as: name }); + exports.push({ name, as: name }); } code.remove(node.start, node.declaration.start); } else { node.specifiers.forEach(specifier => { - this.exports.push({ + exports.push({ name: specifier.local.name, as: specifier.exported.name }); @@ -419,7 +415,7 @@ export default class Component { // imports need to be hoisted out of the IIFE // TODO hoist other stuff where possible else if (node.type === 'ImportDeclaration') { - removeNode(code, js.content, node); + removeNode(code, content.start, content.end, body, node); imports.push(node); node.specifiers.forEach((specifier: Node) => { @@ -428,10 +424,63 @@ export default class Component { }); } }); + } + + walk_module_js() { + const script = this.ast.js.find(script => get_context(script) === 'module'); + if (!script) return; + + this.addSourcemapLocations(script.content); + + // TODO unindent + + this.extract_imports_and_exports(script.content, this.imports, this.module_exports); + + let a = script.content.start; + while (/\s/.test(this.source[a])) a += 1; + + let b = script.content.end; + while (/\s/.test(this.source[b - 1])) b -= 1; + + this.module_javascript = a !== b ? `[✂${a}-${b}✂]` : null; + } + + walk_instance_js() { + const script = this.ast.js.find(script => get_context(script) === 'default'); + if (!script) return; + + this.addSourcemapLocations(script.content); + + const { code, source, imports } = this; + + const indent = code.getIndentString(); + code.indent(indent, { + exclude: [ + [0, script.content.start], + [script.content.end, source.length] + ] + }); + + let { scope, map, globals } = createScopes(script.content); + this.scope = scope; + + scope.declarations.forEach(name => { + this.userVars.add(name); + this.declarations.push(name); + }); + + this.writable_declarations = scope.writable_declarations; + this.initialised_declarations = scope.initialised_declarations; + + globals.forEach(name => { + this.userVars.add(name); + }); + + this.extract_imports_and_exports(script.content, this.imports, this.exports); const top_scope = scope; - walk(js.content, { + walk(script.content, { enter: (node, parent) => { if (map.has(node)) { scope = map.get(node); @@ -453,10 +502,10 @@ export default class Component { } }); - let a = js.content.start; + let a = script.content.start; while (/\s/.test(source[a])) a += 1; - let b = js.content.end; + let b = script.content.end; while (/\s/.test(source[b - 1])) b -= 1; this.javascript = a !== b ? `[✂${a}-${b}✂]` : ''; diff --git a/src/compile/css/Stylesheet.ts b/src/compile/css/Stylesheet.ts index cf7fcf061e..223f865996 100644 --- a/src/compile/css/Stylesheet.ts +++ b/src/compile/css/Stylesheet.ts @@ -264,15 +264,15 @@ export default class Stylesheet { this.nodesWithCssClass = new Set(); this.nodesWithRefCssClass = new Map(); - if (ast.css && ast.css.children.length) { - this.id = `svelte-${hash(ast.css.content.styles)}`; + if (ast.css[0] && ast.css[0].children.length) { + this.id = `svelte-${hash(ast.css[0].content.styles)}`; this.hasStyles = true; const stack: (Rule | Atrule)[] = []; let currentAtrule: Atrule = null; - walk(this.ast.css, { + walk(ast.css[0], { enter: (node: Node) => { if (node.type === 'Atrule') { const last = stack[stack.length - 1]; diff --git a/src/compile/render-dom/index.ts b/src/compile/render-dom/index.ts index 186eacfb64..a066f0c97a 100644 --- a/src/compile/render-dom/index.ts +++ b/src/compile/render-dom/index.ts @@ -219,6 +219,8 @@ export default function dom( }); builder.addBlock(deindent` + ${component.module_javascript} + ${component.fully_hoisted.length > 0 && component.fully_hoisted.join('\n\n')} class ${name} extends ${superclass} { diff --git a/src/compile/wrapModule.ts b/src/compile/wrapModule.ts index f61657b461..55fbc8e64b 100644 --- a/src/compile/wrapModule.ts +++ b/src/compile/wrapModule.ts @@ -19,9 +19,12 @@ export default function wrapModule( sharedPath: string, helpers: { name: string, alias: string }[], imports: Node[], + module_exports: string[], source: string ): string { - if (format === 'es') return es(code, name, options, banner, sharedPath, helpers, imports, source); + if (format === 'es') { + return es(code, name, options, banner, sharedPath, helpers, imports, module_exports, source); + } const dependencies = imports.map((declaration, i) => { const defaultImport = declaration.specifiers.find( @@ -77,6 +80,7 @@ function es( sharedPath: string, helpers: { name: string, alias: string }[], imports: Node[], + module_exports: string[], source: string ) { const importHelpers = helpers.length > 0 && ( @@ -95,7 +99,8 @@ function es( ${importBlock} ${code} - export default ${name};`; + export default ${name}; + ${module_exports.length > 0 && `export { ${module_exports.join(', ')} };`}`; } function amd( diff --git a/src/parse/index.ts b/src/parse/index.ts index c16148631e..891fe0841d 100644 --- a/src/parse/index.ts +++ b/src/parse/index.ts @@ -19,13 +19,13 @@ export class Parser { readonly filename?: string; readonly customElement: CustomElementOptions | true; - index: number; - stack: Array; + index = 0; + stack: Array = []; html: Node; - css: Node; - js: Node; - metaTags: {}; + css: Node[] = []; + js: Node[] = []; + metaTags = {}; allowBindings: boolean; @@ -40,10 +40,6 @@ export class Parser { this.allowBindings = options.bind !== false; - this.index = 0; - this.stack = []; - this.metaTags = {}; - this.html = { start: null, end: null, @@ -51,9 +47,6 @@ export class Parser { children: [], }; - this.css = null; - this.js = null; - this.stack.push(this.html); let state: ParserState = fragment; diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts index f245e85f07..3b46c617c4 100644 --- a/src/parse/state/tag.ts +++ b/src/parse/state/tag.ts @@ -231,16 +231,8 @@ export default function tag(parser: Parser) { if (specials.has(name) && parser.stack.length === 1) { const special = specials.get(name); - if (parser[special.property]) { - parser.index = start; - parser.error({ - code: `duplicate-${name}`, - message: `You can only have one top-level <${name}> tag per component` - }); - } - parser.eat('>', true); - parser[special.property] = special.read(parser, start, element.attributes); + parser[special.property].push(special.read(parser, start, element.attributes)); return; } diff --git a/src/utils/removeNode.ts b/src/utils/removeNode.ts index 5d183b6c94..9d5c4fe2af 100644 --- a/src/utils/removeNode.ts +++ b/src/utils/removeNode.ts @@ -1,46 +1,37 @@ import MagicString from 'magic-string'; import { Node } from '../interfaces'; -const keys = { - ObjectExpression: 'properties', - Program: 'body', -}; - -const offsets = { - ObjectExpression: [1, -1], - Program: [0, 0], -}; - -export function removeNode(code: MagicString, parent: Node, node: Node) { - const key = keys[parent.type]; - const offset = offsets[parent.type]; - if (!key || !offset) throw new Error(`not implemented: ${parent.type}`); - - const list = parent[key]; - const i = list.indexOf(node); +export function removeNode( + code: MagicString, + start: number, + end: number, + body: Node, + node: Node +) { + const i = body.indexOf(node); if (i === -1) throw new Error('node not in list'); let a; let b; - if (list.length === 1) { + if (body.length === 1) { // remove everything, leave {} - a = parent.start + offset[0]; - b = parent.end + offset[1]; + a = start; + b = end; } else if (i === 0) { // remove everything before second node, including comments - a = parent.start + offset[0]; + a = start; while (/\s/.test(code.original[a])) a += 1; - b = list[i].end; + b = body[i].end; while (/[\s,]/.test(code.original[b])) b += 1; } else { // remove the end of the previous node to the end of this one - a = list[i - 1].end; + a = body[i - 1].end; b = node.end; } code.remove(a, b); - list.splice(i, 1); + body.splice(i, 1); return; } \ No newline at end of file