Skip to content

Commit

Permalink
Merge pull request #158 from near/feat/require-export
Browse files Browse the repository at this point in the history
feat: require default or named export
  • Loading branch information
andy-haynes authored Jan 3, 2024
2 parents 7590b8e + 21d803d commit dadf977
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 75 deletions.
127 changes: 96 additions & 31 deletions packages/compiler/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,19 @@ import type {
CompilerInitAction,
ComponentCompilerParams,
ComponentTreeNode,
ModuleImport,
ParseComponentTreeParams,
SendMessageCallback,
TranspiledComponentLookupParams,
TrustedRoot,
} from './types';

interface BuildComponentSourceParams {
componentPath: string;
componentSource: string;
isRoot: boolean;
}

export class ComponentCompiler {
private bosSourceCache: Map<string, Promise<string>>;
private compiledSourceCache: Map<string, string | null>;
Expand All @@ -44,6 +51,68 @@ export class ComponentCompiler {
this.preactVersion = preactVersion;
}

/**
* Build the transpiled source of a BOS Component along with its imports
* @param componentPath path to the BOS Component
* @param componentSource source code of the BOS Component
* @param isRoot flag indicating whether this is the root Component of a container
*/
buildComponentSource({
componentPath,
componentSource,
isRoot,
}: BuildComponentSourceParams): { imports: ModuleImport[]; source: string } {
// transpile and cache the Component
const transpiledComponentSource = this.getTranspiledComponentSource({
componentPath,
componentSource: componentSource,
isRoot,
});

// separate out import statements from Component source
const { imports, source: importlessSource } = extractImportStatements(
transpiledComponentSource
);

// get the exported reference's name and remove the export keyword(s) from Component source
// TODO halt parsing of the current Component if no export is found
const {
exportedReference,
hasExport,
source: cleanComponentSource,
} = extractExport(importlessSource);

if (!hasExport) {
throw new Error(
`Could not parse Component ${componentPath}: missing valid Component export`
);
}

const componentImports = imports
.map((moduleImport) => buildComponentImportStatements(moduleImport))
.flat()
.filter((statement) => !!statement) as string[];

// assign a known alias to the exported Component
const source = buildComponentFunction({
componentPath,
componentSource: cleanComponentSource,
componentImports,
exportedReference,
isRoot,
});

return {
imports,
source,
};
}

/**
* Fetch and cache sources for an array of Component paths
* If a requested path has not been cached, initialize a Promise to resolve the source
* @param componentPaths set of Component paths to fetch source for
*/
getComponentSources(componentPaths: string[]) {
const unfetchedPaths = componentPaths.filter(
(componentPath) => !this.bosSourceCache.has(componentPath)
Expand Down Expand Up @@ -73,6 +142,12 @@ export class ComponentCompiler {
return componentSources;
}

/**
* Transpile the component and cache for future lookups
* @param componentPath path to the BOS Component
* @param componentSource source code of the BOS Component
* @param isRoot flag indicating whether this is the root Component of a container
*/
getTranspiledComponentSource({
componentPath,
componentSource,
Expand All @@ -92,17 +167,25 @@ export class ComponentCompiler {
return this.compiledSourceCache.get(cacheKey)!;
}

/**
* Determine whether a child Component is trusted and can be inlined within the current container
* @param trustMode explicit trust mode provided for this child render
* @param path child Component's path
* @param isComponentPathTrusted flag indicating whether the child is implicitly trusted by virtue of being under a trusted root
*/
static isChildComponentTrusted(
{ trustMode, path }: ParsedChildComponent,
isComponentPathTrusted?: (p: string) => boolean
) {
// child is explicitly trusted by parent or constitutes a new trusted root
if (
trustMode === TrustMode.Trusted ||
trustMode === TrustMode.TrustAuthor
) {
return true;
}

// child is explicitly sandboxed
if (trustMode === TrustMode.Sandboxed) {
return false;
}
Expand Down Expand Up @@ -133,38 +216,15 @@ export class ComponentCompiler {
isRoot,
trustedRoot,
}: ParseComponentTreeParams) {
// separate out import statements from Component source
const { imports, source: importlessSource } =
extractImportStatements(componentSource);

// get the exported reference's name and remove the export keyword(s) from Component source
// TODO halt parsing of the current Component if no export is found
const { exported, source: cleanComponentSource } =
extractExport(importlessSource);

const componentImports = imports
.map((moduleImport) => buildComponentImportStatements(moduleImport))
.flat()
.filter((statement) => !!statement) as string[];

// assign a known alias to the exported Component
const componentFunctionSource = buildComponentFunction({
componentPath,
componentSource: cleanComponentSource,
componentImports,
exported,
isRoot,
});

// transpile and cache the Component
const transpiledComponent = this.getTranspiledComponentSource({
componentPath,
componentSource: componentFunctionSource,
isRoot,
});
const { imports, source: componentFunctionSource } =
this.buildComponentSource({
componentPath,
componentSource,
isRoot,
});

// enumerate the set of Components referenced in the target Component
const childComponents = parseChildComponents(transpiledComponent);
const childComponents = parseChildComponents(componentFunctionSource);

// each child Component being rendered as a new trusted root (i.e. trust mode `trusted-author`)
// will track inclusion criteria when evaluating trust for their children in turn
Expand Down Expand Up @@ -201,7 +261,7 @@ export class ComponentCompiler {
transpiled: trustedChildComponents.reduce(
(transformed, { path, transform }) =>
transform(transformed, buildComponentFunctionName(path)),
transpiledComponent
componentFunctionSource
),
});

Expand Down Expand Up @@ -243,6 +303,10 @@ export class ComponentCompiler {
return components;
}

/**
* Build the source for a container rooted at the target Component
* @param componentId ID for the new container's root Component
*/
async compileComponent({ componentId }: CompilerExecuteAction) {
if (this.localFetchUrl && !this.hasFetchedLocal) {
try {
Expand Down Expand Up @@ -273,6 +337,7 @@ export class ComponentCompiler {
.map(({ imports }) => imports)
.flat();

// build the import map used by the container
const importedModules = containerModuleImports.reduce(
(importMap, { moduleName, modulePath }) => {
const importMapEntries = buildModulePackageUrl(
Expand Down
28 changes: 3 additions & 25 deletions packages/compiler/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,49 +15,27 @@ interface BuildComponentFunctionParams {
componentImports: string[];
componentPath: string;
componentSource: string;
exported: string | null;
exportedReference: string | null;
isRoot: boolean;
}

export function buildComponentFunction({
componentImports,
componentPath,
componentSource,
exported,
exportedReference,
isRoot,
}: BuildComponentFunctionParams) {
const functionName = buildComponentFunctionName(isRoot ? '' : componentPath);
const importAssignments = componentImports.join('\n');
const commentHeader = `${componentPath} ${isRoot ? '(root)' : ''}`;

// TODO remove once export is required
if (!exported) {
if (isRoot) {
return `
function ${functionName}() {
${importAssignments}
${componentSource}
}
`;
}

return `
/************************* ${componentPath} *************************/
function ${functionName}(__bweInlineComponentProps) {
${importAssignments}
const { __bweMeta, props: __componentProps } = __bweInlineComponentProps;
const props = Object.assign({ __bweMeta }, __componentProps);
${componentSource}
}
`;
}

return `
/************************* ${commentHeader} *************************/
const ${functionName} = (() => {
${importAssignments}
${componentSource}
return ${exported};
return ${exportedReference ? exportedReference : 'BWEComponent'};
})();
`;
}
59 changes: 42 additions & 17 deletions packages/compiler/src/export.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,56 @@
/**
* Supported export syntax:
* - export default X;
* - export function X...
* - export const X...
* - export function BWEComponent...
* - export const BWEComponent...
*/
const EXPORT_REGEX =
/^export\s+(const|default|function)\s+(?<identifier>[\w$_]+)/g;
/^export(?<defaultExport>\s+default)?\s+(const|function)\s+(?<identifier>[\w$_]+)?/gm;

/**
* Extract the name of the exported reference and strip the export keyword(s) from the source
* @param source BOS Component source
*/
export const extractExport = (source: string) => {
const [match, ...matches] = [...source.matchAll(EXPORT_REGEX)];
if (matches.length) {
throw new Error(`Multiple exports not permitted: ${matches.join(', ')}`);
}
return [...source.matchAll(EXPORT_REGEX)].reduce(
(exported, match) => {
if (!exported.hasExport) {
const { defaultExport, identifier } = match.groups as {
defaultExport: string;
identifier: string;
};

if (!match?.groups?.identifier) {
return { exported: null, source };
}
if (defaultExport) {
if (!identifier) {
exported.source = exported.source
.replace(
/export\s+default\s+function\s*\(/,
'function BWEComponent('
)
.replace(/export\s+default\s+\(/, 'const BWEComponent = (');
} else {
exported.exportedReference = identifier;
}
exported.hasExport = true;
} else if (identifier === 'BWEComponent') {
exported.hasExport = true;
}
}

return {
exported: match.groups.identifier,
source: source.replace(
match[0],
match[0].replace(/export(\s+default)?\s+/, '')
),
};
return {
...exported,
source: exported.source.replace(
match[0],
match[0]
.replace(/export\s+default\s+/, '') // replace "export default"
.replace(/export\s+(const|function)/, '$1') // remove "export" from export statements
),
};
},
{
exportedReference: '',
hasExport: false,
source,
}
);
};
30 changes: 28 additions & 2 deletions packages/compiler/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,36 @@ export function parseChildComponents(
path: source,
trustMode: trustMatch?.[1],
transform: (componentSource: string, componentName: string) => {
const signaturePrefix = `${componentName},{__bweMeta:{parentMeta:props.__bweMeta},`;
const propsMatch = expression.match(/\s+props:\s*/);

if (!propsMatch?.index) {
return componentSource.replaceAll(
expression,
expression.replace(/(Widget|Component)/, componentName)
);
}

const openPropsBracketIndex = propsMatch.index + propsMatch[0].length;
let closePropsBracketIndex = openPropsBracketIndex + 1;
let openBracketCount = 1;
while (openBracketCount) {
const char = expression[closePropsBracketIndex];
if (char === '{') {
openBracketCount++;
} else if (char === '}') {
openBracketCount--;
}

closePropsBracketIndex++;
}

const propsString = expression
.slice(openPropsBracketIndex + 1, closePropsBracketIndex - 1)
.trim();

return componentSource.replaceAll(
expression,
expression.replace(/(Widget|Component),\s*\{/, signaturePrefix)
`createElement(${componentName}, { __bweMeta: { parentMeta: props.__bweMeta }, ${propsString} })`
);
},
};
Expand Down

0 comments on commit dadf977

Please sign in to comment.