Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] feat(compiler): handle error #67

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions packages/build-plugin-pwc/src/utils/error.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
import type { RollupError } from 'rollup';
import type { CompilerError } from '@pwc/compiler';

export function createRollupError(
id: string,
error: SyntaxError,
error: SyntaxError | CompilerError,
): RollupError {
return {
id,
plugin: 'pwc',
message: error.message,
parserError: error,
};
if ('code' in error) {
return {
id,
plugin: 'pwc',
pluginCode: String(error.code),
message: error.message,
frame: error.loc!.source,
parserError: error,
loc: error.loc
? {
file: id,
line: error.loc.start.line,
column: error.loc.start.column,
}
: undefined,
};
} else {
return {
id,
plugin: 'pwc',
message: error.message,
parserError: error,
};
}
}
3 changes: 1 addition & 2 deletions packages/pwc-compiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,13 @@
"npm": ">=3.0.0"
},
"dependencies": {
"@babel/core": "^7.17.9",
"@babel/generator": "^7.17.7",
"@babel/parser": "^7.17.9",
"@babel/plugin-proposal-decorators": "^7.17.2",
"@babel/traverse": "^7.17.3",
"@babel/types": "^7.17.0",
"@swc/helpers": "^0.3.8",
"parse5": "^6.0.1",
"parse5": "^7.0.0",
"postcss": "^8.4.12",
"rfdc": "^1.3.0",
"source-map": "^0.7.3"
Expand Down
9 changes: 8 additions & 1 deletion packages/pwc-compiler/src/compileScript.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import generate from '@babel/generator';
import { join } from 'path';
import rfdc from 'rfdc';
import type { SFCDescriptor, SFCScriptBlock } from './parse.js';
import { compileTemplate } from './compileTemplate.js';
Expand All @@ -8,9 +9,11 @@ const deepClone = rfdc();

export interface SFCScriptCompileResult extends SFCScriptBlock {
filename: string;
sourceRoot: string;
}

export function compileScript(descriptor: SFCDescriptor): SFCScriptCompileResult {
const rootContext = process.cwd();
const { script, filename } = descriptor;
const ast = deepClone(descriptor.script.ast);

Expand All @@ -26,15 +29,19 @@ export function compileScript(descriptor: SFCDescriptor): SFCScriptCompileResult
transformScript(ast, { templateString: null });
}

const sourceRoot = join(rootContext, 'src');
const { code, map } = generate.default(ast, {
sourceMaps: false,
sourceMaps: true,
sourceRoot,
sourceFileName: filename,
decoratorsBeforeExport: true,
});

// TODO: source map
return {
...script,
filename,
sourceRoot,
content: code,
map,
};
Expand Down
46 changes: 46 additions & 0 deletions packages/pwc-compiler/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { SourceLocation } from './parse.js';

export interface CompilerError extends SyntaxError {
code: number | string;
loc?: SourceLocation;
}

export function createCompilerError(
code: number | string,
loc?: SourceLocation,
): CompilerError {
const msg = typeof code === 'string' ? code : errorMessages[code];
const error = new SyntaxError(String(msg)) as CompilerError;
error.code = code;
error.loc = loc;
return error;
}

export const enum ErrorCodes {
// parse errors
MISSING_SCRIPT_TAG,
DUPLICATE_SCRIPT_TAG,
DUPLICATE_TEMPLATE_TAG,
DUPLICATE_STYLE_TAG,
// script errors
MISSING_EXPORT_DEFAULT,
MISSING_EXPORT_CLASS,
MISSING_EXPORT_CLASS_EXTENDED_FROM_HTMLELEMENT,
// transform errors
X_V_IF_NO_EXPRESSION,
}

export const errorMessages: Record<ErrorCodes, string> = {
// parse errors
[ErrorCodes.MISSING_SCRIPT_TAG]: 'PWC must contain one <script> tag.',
[ErrorCodes.DUPLICATE_SCRIPT_TAG]: 'PWC mustn\'t contain more than one <script> tag.',
[ErrorCodes.DUPLICATE_TEMPLATE_TAG]: 'PWC mustn\'t contain more than one <template> tag.',
[ErrorCodes.DUPLICATE_STYLE_TAG]: 'PWC mustn\'t contain more than one <template> tag.',

// script errors
[ErrorCodes.MISSING_EXPORT_DEFAULT]: 'PWC must allow one export default',
[ErrorCodes.MISSING_EXPORT_CLASS]: 'PWC must export a class',
[ErrorCodes.MISSING_EXPORT_CLASS_EXTENDED_FROM_HTMLELEMENT]: 'PWC must export a class extended from HTMLElement',
// transform errors
[ErrorCodes.X_V_IF_NO_EXPRESSION]: 'x-if/x-else-if is missing expression.',
};
1 change: 1 addition & 0 deletions packages/pwc-compiler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './parse.js';
export * from './compileScript.js';
export * from './compileTemplate.js';
export * from './compileStyle.js';
export * from './errors.js';
export { compileTemplateInRuntime } from './compileTemplateInRuntime.js';
136 changes: 95 additions & 41 deletions packages/pwc-compiler/src/parse.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { parseFragment } from 'parse5';
import { type Token, parseFragment } from 'parse5';
import * as babelParser from '@babel/parser';
import type { File } from '@babel/types';
import type { RawSourceMap } from 'source-map';
import { SourceMapGenerator } from 'source-map';
import { validateScript, validateTemplate } from './validate.js';
import { type CompilerError, createCompilerError, ErrorCodes } from './errors.js';

export interface SFCBlock {
type: string;
Expand Down Expand Up @@ -48,13 +49,18 @@ export interface SFCParseResult {
errors: SyntaxError[];
}

export interface SourceLocation {
startLine: number;
startCol: number;
startOffset: number;
endLine: number;
endCol: number;
endOffset: number;
export interface Position {
line: number;
column: number;
offset?: number;
}

export interface Location {
start: Position;
end: Position;
}

export interface SourceLocation extends Location {
source: string;
}

Expand All @@ -68,39 +74,35 @@ export interface ElementNode {
value?: string;
data?: string;
attrs?: AttributeNode[];
content?: ElementNode;
childNodes?: ElementNode[];
parentNode?: ElementNode;
sourceCodeLocation?: SourceLocation;
loc?: SourceLocation;
}

function createSourceLocation(originalSource: string, locationInfo: Location): SourceLocation {
const { start, end } = locationInfo;
const source = originalSource.slice(start.offset, end.offset);
return { start, end, source };
}

function createBlock(
node: ElementNode,
source: string,
): SFCBlock {
const type = node.nodeName;
let { startLine, startCol, startOffset, endLine, endCol, endOffset } = node.sourceCodeLocation;
let { start, end } = node.loc;
let content = '';

if (node.childNodes.length) {
const startNodeLocation = node.childNodes[0].sourceCodeLocation;
const endNodeLocation = node.childNodes[node.childNodes.length - 1].sourceCodeLocation;
content = source.slice(startNodeLocation.startOffset, endNodeLocation.endOffset);

startLine = startNodeLocation.startLine;
startCol = startNodeLocation.startCol;
startOffset = startNodeLocation.startOffset;
endLine = endNodeLocation.endLine;
endCol = endNodeLocation.endCol;
endOffset = endNodeLocation.endOffset;
const { start } = node.childNodes[0].loc;
const { end } = node.childNodes[node.childNodes.length - 1].loc;
content = source.slice(start.offset, end.offset);
}
const loc = {
source: content,
startLine,
startCol,
startOffset,
endLine,
endCol,
endOffset,
start,
end,
};
const attrs: Record<string, string | true> = {};
const block: SFCBlock = {
Expand All @@ -109,7 +111,7 @@ function createBlock(
loc,
attrs,
};
node.attrs.forEach((attr) => {
node.attrs?.forEach((attr) => {
attrs[attr.name] = attr.value ? attr.value || true : true;
if (attr.name === 'lang') {
block.lang = attr.value && attr.value;
Expand Down Expand Up @@ -164,11 +166,41 @@ function isEmptyString(str: string): boolean {
return str.replace(/(^\s*)|(\s*$)/g, '').length === 0;
}

function transformSourceCodeLocation(root: any): void {
setNodeSourceCodeLocation(root, root.sourceCodeLocation);
if (root.childNodes?.length) {
root.childNodes.forEach(node => transformSourceCodeLocation(node));
}
if (root.content) {
transformSourceCodeLocation(root.content);
}
}

function setNodeSourceCodeLocation(node: any, location: Token.ElementLocation | null): void {
if (location) {
const { startLine, startCol, startOffset, endLine, endCol, endOffset } = location;
node.loc = {
start: {
line: startLine,
column: startCol,
offset: startOffset,
},
end: {
line: endLine,
column: endCol,
offset: endOffset,
},
};
}
}

export function parse(source: string, {
filename = 'anonymous.pwc',
sourceRoot = '',
sourceMap = true,
}: SFCParseOptions = {}): SFCParseResult {
let errors: (CompilerError | SyntaxError)[] = [];

const descriptor: SFCDescriptor = {
filename,
source,
Expand All @@ -177,27 +209,42 @@ export function parse(source: string, {
style: null,
};

let dom;
try {
dom = parseFragment(source, { sourceCodeLocationInfo: true });
} catch (err) {
throw new Error(`[@pwc/compiler] compile error: ${err}`);
}

let errors = [];
const dom = parseFragment(source,
{
sourceCodeLocationInfo: true,
onParseError: (parseError) => {
const { code, startLine, startCol, startOffset, endLine, endCol, endOffset } = parseError;
const err = createCompilerError(code, {
start: { line: startLine, column: startCol, offset: startOffset },
end: { line: endLine, column: endCol, offset: endOffset },
source,
});
errors.push(err);
}
});
transformSourceCodeLocation(dom);

// Check phase 1: sfc
const scriptNodeAmount = dom.childNodes.filter(node => node.nodeName === 'script').length;
if (scriptNodeAmount === 0) {
errors.push(new Error('[@pwc/compiler] PWC must contain one <script> tag.'));
const err = createCompilerError(ErrorCodes.MISSING_SCRIPT_TAG, {
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 1, offset: 0 },
source,
});
errors.push(err);
}

for (const node of dom.childNodes) {
for (let index = 0, length = dom.childNodes.length; index < length; index++) {
const node = dom.childNodes[index] as ElementNode;
if (node.nodeName === 'template') {
// TODO: Check phase 2: template
if (descriptor.template) {
errors.push(new Error('[@pwc/compiler] PWC mustn\'t contain more than one <template> tag.'));
const loc = createSourceLocation(source, node.loc);
const err = createCompilerError(ErrorCodes.DUPLICATE_TEMPLATE_TAG, loc);
errors.push(err);
} else {
// Template node contains a content property which is the parentNode of the template node's childNodes
node.childNodes = node.content.childNodes;
const templateBlock = createBlock(node, source) as SFCTemplateBlock;
templateBlock.ast = node;
Expand All @@ -209,18 +256,22 @@ export function parse(source: string, {
if (node.nodeName === 'script') {
// TODO:Check phase 3: script
if (descriptor.script) {
errors.push(new Error('[@pwc/compiler] PWC mustn\'t contain more than only one <script> tag.'));
const loc = createSourceLocation(source, node.loc);
const err = createCompilerError(ErrorCodes.DUPLICATE_SCRIPT_TAG, loc);
errors.push(err);
} else {
const scriptBlock = createBlock(node, source) as SFCScriptBlock;
if (!isEmptyString(scriptBlock.content)) {
const ast = babelParser.parse(scriptBlock.content, {
sourceType: 'module',
sourceFilename: filename,
startLine: node.loc.start.line,
plugins: [
['decorators', { decoratorsBeforeExport: true }],
'decoratorAutoAccessors',
],
});
errors = errors.concat(validateScript(ast));
errors = errors.concat(validateScript(ast, scriptBlock.content));

scriptBlock.ast = ast;
descriptor.script = scriptBlock;
Expand All @@ -230,7 +281,9 @@ export function parse(source: string, {
if (node.nodeName === 'style') {
// TODO:Check phase 4: style
if (descriptor.style) {
errors.push(new Error('[@pwc/compiler] PWC mustn\'t contain more than one <style> tag.'));
const loc = createSourceLocation(source, node.loc);
const err = createCompilerError(ErrorCodes.DUPLICATE_STYLE_TAG, loc);
errors.push(err);
} else {
const styleBlock = createBlock(node, source) as SFCStyleBlock;
if (!isEmptyString(styleBlock.content)) {
Expand All @@ -256,6 +309,7 @@ export function parse(source: string, {
genMap(descriptor.style);
}
}

return {
descriptor,
errors,
Expand Down
Loading