|
|
|
@ -20,6 +20,12 @@ import shared from './shared';
|
|
|
|
|
import { DomTarget } from './dom';
|
|
|
|
|
import { SsrTarget } from './ssr';
|
|
|
|
|
import { Node, GenerateOptions, ShorthandImport, Ast, CompileOptions, CustomElementOptions } from '../interfaces';
|
|
|
|
|
import error from '../utils/error';
|
|
|
|
|
import getCodeFrame from '../utils/getCodeFrame';
|
|
|
|
|
import checkForComputedKeys from '../validate/js/utils/checkForComputedKeys';
|
|
|
|
|
import checkForDupes from '../validate/js/utils/checkForDupes';
|
|
|
|
|
import propValidators from '../validate/js/propValidators';
|
|
|
|
|
import fuzzymatch from '../validate/utils/fuzzymatch';
|
|
|
|
|
|
|
|
|
|
interface Computation {
|
|
|
|
|
key: string;
|
|
|
|
@ -92,6 +98,8 @@ export default class Component {
|
|
|
|
|
tag: string;
|
|
|
|
|
props: string[];
|
|
|
|
|
|
|
|
|
|
properties: Map<string, Node>;
|
|
|
|
|
|
|
|
|
|
defaultExport: Node[];
|
|
|
|
|
imports: Node[];
|
|
|
|
|
shorthandImports: ShorthandImport[];
|
|
|
|
@ -110,6 +118,17 @@ export default class Component {
|
|
|
|
|
slots: Set<string>;
|
|
|
|
|
javascript: string;
|
|
|
|
|
|
|
|
|
|
used: {
|
|
|
|
|
components: Set<string>;
|
|
|
|
|
helpers: Set<string>;
|
|
|
|
|
events: Set<string>;
|
|
|
|
|
animations: Set<string>;
|
|
|
|
|
transitions: Set<string>;
|
|
|
|
|
actions: Set<string>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
refCallees: Node[];
|
|
|
|
|
|
|
|
|
|
code: MagicString;
|
|
|
|
|
|
|
|
|
|
bindingGroups: string[];
|
|
|
|
@ -128,16 +147,19 @@ export default class Component {
|
|
|
|
|
aliases: Map<string, string>;
|
|
|
|
|
usedNames: Set<string>;
|
|
|
|
|
|
|
|
|
|
locator: (search: number, startIndex?: number) => {
|
|
|
|
|
line: number,
|
|
|
|
|
column: number
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
ast: Ast,
|
|
|
|
|
source: string,
|
|
|
|
|
name: string,
|
|
|
|
|
stylesheet: Stylesheet,
|
|
|
|
|
options: CompileOptions,
|
|
|
|
|
stats: Stats,
|
|
|
|
|
target: DomTarget | SsrTarget
|
|
|
|
|
) {
|
|
|
|
|
stats.start('compile');
|
|
|
|
|
this.stats = stats;
|
|
|
|
|
|
|
|
|
|
this.ast = ast;
|
|
|
|
@ -157,6 +179,17 @@ export default class Component {
|
|
|
|
|
this.importedComponents = new Map();
|
|
|
|
|
this.slots = new Set();
|
|
|
|
|
|
|
|
|
|
this.used = {
|
|
|
|
|
components: new Set(),
|
|
|
|
|
helpers: new Set(),
|
|
|
|
|
events: new Set(),
|
|
|
|
|
animations: new Set(),
|
|
|
|
|
transitions: new Set(),
|
|
|
|
|
actions: new Set(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.refCallees = [];
|
|
|
|
|
|
|
|
|
|
this.bindingGroups = [];
|
|
|
|
|
this.indirectDependencies = new Map();
|
|
|
|
|
|
|
|
|
@ -173,7 +206,7 @@ export default class Component {
|
|
|
|
|
this.usesRefs = false;
|
|
|
|
|
|
|
|
|
|
// styles
|
|
|
|
|
this.stylesheet = stylesheet;
|
|
|
|
|
this.stylesheet = new Stylesheet(source, ast, this.file, options.dev);
|
|
|
|
|
|
|
|
|
|
// allow compiler to deconflict user's `import { get } from 'whatever'` and
|
|
|
|
|
// Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`;
|
|
|
|
@ -186,6 +219,7 @@ export default class Component {
|
|
|
|
|
|
|
|
|
|
this.computations = [];
|
|
|
|
|
this.templateProperties = {};
|
|
|
|
|
this.properties = new Map();
|
|
|
|
|
|
|
|
|
|
this.walkJs();
|
|
|
|
|
this.name = this.alias(name);
|
|
|
|
@ -207,7 +241,7 @@ export default class Component {
|
|
|
|
|
// this.walkTemplate();
|
|
|
|
|
if (!this.customElement) this.stylesheet.reify();
|
|
|
|
|
|
|
|
|
|
stylesheet.warnOnUnusedSelectors(options.onwarn);
|
|
|
|
|
this.stylesheet.warnOnUnusedSelectors(options.onwarn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addSourcemapLocations(node: Node) {
|
|
|
|
@ -382,8 +416,6 @@ export default class Component {
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.stats.stop('compile');
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
ast: this.ast,
|
|
|
|
|
js,
|
|
|
|
@ -430,57 +462,128 @@ export default class Component {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
walkJs() {
|
|
|
|
|
const {
|
|
|
|
|
code,
|
|
|
|
|
source,
|
|
|
|
|
computations,
|
|
|
|
|
templateProperties,
|
|
|
|
|
imports
|
|
|
|
|
} = this;
|
|
|
|
|
validate() {
|
|
|
|
|
const { filename } = this.options;
|
|
|
|
|
|
|
|
|
|
const { js } = this.ast;
|
|
|
|
|
try {
|
|
|
|
|
if (this.stylesheet) {
|
|
|
|
|
this.stylesheet.validate(this);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (onerror) {
|
|
|
|
|
onerror(err);
|
|
|
|
|
} else {
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const componentDefinition = new CodeBuilder();
|
|
|
|
|
error(
|
|
|
|
|
pos: {
|
|
|
|
|
start: number,
|
|
|
|
|
end: number
|
|
|
|
|
},
|
|
|
|
|
e : {
|
|
|
|
|
code: string,
|
|
|
|
|
message: string
|
|
|
|
|
}
|
|
|
|
|
) {
|
|
|
|
|
error(e.message, {
|
|
|
|
|
name: 'ValidationError',
|
|
|
|
|
code: e.code,
|
|
|
|
|
source: this.source,
|
|
|
|
|
start: pos.start,
|
|
|
|
|
end: pos.end,
|
|
|
|
|
filename: this.options.filename
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (js) {
|
|
|
|
|
this.addSourcemapLocations(js.content);
|
|
|
|
|
warn(
|
|
|
|
|
pos: {
|
|
|
|
|
start: number,
|
|
|
|
|
end: number
|
|
|
|
|
},
|
|
|
|
|
warning: {
|
|
|
|
|
code: string,
|
|
|
|
|
message: string
|
|
|
|
|
}
|
|
|
|
|
) {
|
|
|
|
|
if (!this.locator) {
|
|
|
|
|
this.locator = getLocator(this.source, { offsetLine: 1 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const indentation = detectIndentation(source.slice(js.start, js.end));
|
|
|
|
|
const indentationLevel = getIndentationLevel(source, js.content.body[0].start);
|
|
|
|
|
const indentExclusionRanges = getIndentExclusionRanges(js.content);
|
|
|
|
|
const start = this.locator(pos.start);
|
|
|
|
|
const end = this.locator(pos.end);
|
|
|
|
|
|
|
|
|
|
const { scope, globals } = annotateWithScopes(js.content);
|
|
|
|
|
const frame = getCodeFrame(this.source, start.line - 1, start.column);
|
|
|
|
|
|
|
|
|
|
scope.declarations.forEach(name => {
|
|
|
|
|
this.userVars.add(name);
|
|
|
|
|
this.stats.warn({
|
|
|
|
|
code: warning.code,
|
|
|
|
|
message: warning.message,
|
|
|
|
|
frame,
|
|
|
|
|
start,
|
|
|
|
|
end,
|
|
|
|
|
pos: pos.start,
|
|
|
|
|
filename: this.options.filename,
|
|
|
|
|
toString: () => `${warning.message} (${start.line + 1}:${start.column})\n${frame}`,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
globals.forEach(name => {
|
|
|
|
|
this.userVars.add(name);
|
|
|
|
|
processDefaultExport(node, componentDefinition, indentExclusionRanges) {
|
|
|
|
|
const { templateProperties, source, code } = this;
|
|
|
|
|
|
|
|
|
|
if (node.declaration.type !== 'ObjectExpression') {
|
|
|
|
|
this.error(node.declaration, {
|
|
|
|
|
code: `invalid-default-export`,
|
|
|
|
|
message: `Default export must be an object literal`
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const body = js.content.body.slice(); // slice, because we're going to be mutating the original
|
|
|
|
|
checkForComputedKeys(this, node.declaration.properties);
|
|
|
|
|
checkForDupes(this, node.declaration.properties);
|
|
|
|
|
|
|
|
|
|
// imports need to be hoisted out of the IIFE
|
|
|
|
|
for (let i = 0; i < body.length; i += 1) {
|
|
|
|
|
const node = body[i];
|
|
|
|
|
if (node.type === 'ImportDeclaration') {
|
|
|
|
|
removeNode(code, js.content, node);
|
|
|
|
|
imports.push(node);
|
|
|
|
|
const props = this.properties;
|
|
|
|
|
|
|
|
|
|
node.specifiers.forEach((specifier: Node) => {
|
|
|
|
|
this.userVars.add(specifier.local.name);
|
|
|
|
|
node.declaration.properties.forEach((prop: Node) => {
|
|
|
|
|
props.set(getName(prop.key), prop);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const validPropList = Object.keys(propValidators);
|
|
|
|
|
|
|
|
|
|
// ensure all exported props are valid
|
|
|
|
|
node.declaration.properties.forEach((prop: Node) => {
|
|
|
|
|
const name = getName(prop.key);
|
|
|
|
|
const propValidator = propValidators[name];
|
|
|
|
|
|
|
|
|
|
if (propValidator) {
|
|
|
|
|
propValidator(this, prop);
|
|
|
|
|
} else {
|
|
|
|
|
const match = fuzzymatch(name, validPropList);
|
|
|
|
|
if (match) {
|
|
|
|
|
this.error(prop, {
|
|
|
|
|
code: `unexpected-property`,
|
|
|
|
|
message: `Unexpected property '${name}' (did you mean '${match}'?)`
|
|
|
|
|
});
|
|
|
|
|
} else if (/FunctionExpression/.test(prop.value.type)) {
|
|
|
|
|
this.error(prop, {
|
|
|
|
|
code: `unexpected-property`,
|
|
|
|
|
message: `Unexpected property '${name}' (did you mean to include it in 'methods'?)`
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
this.error(prop, {
|
|
|
|
|
code: `unexpected-property`,
|
|
|
|
|
message: `Unexpected property '${name}'`
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const defaultExport = this.defaultExport = body.find(
|
|
|
|
|
(node: Node) => node.type === 'ExportDefaultDeclaration'
|
|
|
|
|
);
|
|
|
|
|
if (props.has('namespace')) {
|
|
|
|
|
const ns = nodeToString(props.get('namespace').value);
|
|
|
|
|
this.namespace = namespaces[ns] || ns;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (defaultExport) {
|
|
|
|
|
defaultExport.declaration.properties.forEach((prop: Node) => {
|
|
|
|
|
node.declaration.properties.forEach((prop: Node) => {
|
|
|
|
|
templateProperties[getName(prop.key)] = prop;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@ -716,12 +819,68 @@ export default class Component {
|
|
|
|
|
addDeclaration(getName(property.key), property.value, false, 'actions');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.defaultExport = node;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
walkJs() {
|
|
|
|
|
const {
|
|
|
|
|
code,
|
|
|
|
|
source,
|
|
|
|
|
imports
|
|
|
|
|
} = this;
|
|
|
|
|
|
|
|
|
|
const { js } = this.ast;
|
|
|
|
|
|
|
|
|
|
const componentDefinition = new CodeBuilder();
|
|
|
|
|
|
|
|
|
|
if (js) {
|
|
|
|
|
this.addSourcemapLocations(js.content);
|
|
|
|
|
|
|
|
|
|
const indentation = detectIndentation(source.slice(js.start, js.end));
|
|
|
|
|
const indentationLevel = getIndentationLevel(source, js.content.body[0].start);
|
|
|
|
|
const indentExclusionRanges = getIndentExclusionRanges(js.content);
|
|
|
|
|
|
|
|
|
|
const { scope, globals } = annotateWithScopes(js.content);
|
|
|
|
|
|
|
|
|
|
scope.declarations.forEach(name => {
|
|
|
|
|
this.userVars.add(name);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
globals.forEach(name => {
|
|
|
|
|
this.userVars.add(name);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const body = js.content.body.slice(); // slice, because we're going to be mutating the original
|
|
|
|
|
|
|
|
|
|
body.forEach(node => {
|
|
|
|
|
// check there are no named exports
|
|
|
|
|
if (node.type === 'ExportNamedDeclaration') {
|
|
|
|
|
this.error(node, {
|
|
|
|
|
code: `named-export`,
|
|
|
|
|
message: `A component can only have a default export`
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (node.type === 'ExportDefaultDeclaration') {
|
|
|
|
|
this.processDefaultExport(node, componentDefinition, indentExclusionRanges);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// imports need to be hoisted out of the IIFE
|
|
|
|
|
else if (node.type === 'ImportDeclaration') {
|
|
|
|
|
removeNode(code, js.content, node);
|
|
|
|
|
imports.push(node);
|
|
|
|
|
|
|
|
|
|
node.specifiers.forEach((specifier: Node) => {
|
|
|
|
|
this.userVars.add(specifier.local.name);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (indentationLevel) {
|
|
|
|
|
if (defaultExport) {
|
|
|
|
|
removeIndentation(code, js.content.start, defaultExport.start, indentationLevel, indentExclusionRanges);
|
|
|
|
|
removeIndentation(code, defaultExport.end, js.content.end, indentationLevel, indentExclusionRanges);
|
|
|
|
|
if (this.defaultExport) {
|
|
|
|
|
removeIndentation(code, js.content.start, this.defaultExport.start, indentationLevel, indentExclusionRanges);
|
|
|
|
|
removeIndentation(code, this.defaultExport.end, js.content.end, indentationLevel, indentExclusionRanges);
|
|
|
|
|
} else {
|
|
|
|
|
removeIndentation(code, js.content.start, js.content.end, indentationLevel, indentExclusionRanges);
|
|
|
|
|
}
|
|
|
|
@ -733,11 +892,11 @@ export default class Component {
|
|
|
|
|
let b = js.content.end;
|
|
|
|
|
while (/\s/.test(source[b - 1])) b -= 1;
|
|
|
|
|
|
|
|
|
|
if (defaultExport) {
|
|
|
|
|
if (this.defaultExport) {
|
|
|
|
|
this.javascript = '';
|
|
|
|
|
if (a !== defaultExport.start) this.javascript += `[✂${a}-${defaultExport.start}✂]`;
|
|
|
|
|
if (a !== this.defaultExport.start) this.javascript += `[✂${a}-${this.defaultExport.start}✂]`;
|
|
|
|
|
if (!componentDefinition.isEmpty()) this.javascript += componentDefinition;
|
|
|
|
|
if (defaultExport.end !== b) this.javascript += `[✂${defaultExport.end}-${b}✂]`;
|
|
|
|
|
if (this.defaultExport.end !== b) this.javascript += `[✂${this.defaultExport.end}-${b}✂]`;
|
|
|
|
|
} else {
|
|
|
|
|
this.javascript = a === b ? null : `[✂${a}-${b}✂]`;
|
|
|
|
|
}
|
|
|
|
|