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

feat: imports #151

Merged
merged 16 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from 10 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
6 changes: 5 additions & 1 deletion apps/web/components/ComponentTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,18 @@ export default function ComponentTree({
{Object.entries(components)
.filter(([, component]) => !!component?.componentSource)
.map(
([componentId, { trust, props, componentSource, parentId }]) => (
([
componentId,
{ trust, props, componentSource, parentId, moduleImports },
]) => (
<div key={componentId} component-id={componentId}>
<SandboxedIframe
id={getIframeId(componentId)}
trust={trust}
scriptSrc={componentSource}
componentProps={props}
parentContainerId={parentId}
moduleImports={moduleImports}
/>
</div>
)
Expand Down
26 changes: 26 additions & 0 deletions apps/web/hooks/useWebEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ import { useComponentMetrics } from './useComponentMetrics';
import { useFlags } from './useFlags';
import { useComponentSourcesStore } from '../stores/component-sources';

interface WebEngineConfiguration {
preactVersion: string;
}

interface UseWebEngineParams {
config?: WebEngineConfiguration;
rootComponentPath?: string;
debugConfig: DebugConfig;
}
Expand All @@ -34,7 +39,12 @@ interface CompilerWorker extends Omit<Worker, 'postMessage'> {
postMessage(comilerRequest: ComponentCompilerRequest): void;
}

const DEFAULT_CONFIG = {
preactVersion: '10.17.1',
};

export function useWebEngine({
config = DEFAULT_CONFIG,
rootComponentPath,
debugConfig,
}: UseWebEngineParams) {
Expand Down Expand Up @@ -217,6 +227,7 @@ export function useWebEngine({
const initPayload: ComponentCompilerRequest = {
action: 'init',
localFetchUrl: flags?.bosLoaderUrl,
preactVersion: config?.preactVersion,
};
worker.postMessage(initPayload);
setCompiler(worker);
Expand All @@ -232,6 +243,7 @@ export function useWebEngine({
rawSource,
componentPath,
error: loadError,
importedModules,
} = data;

if (loadError) {
Expand All @@ -241,11 +253,24 @@ export function useWebEngine({

addSource(componentPath, rawSource);

// set the Preact import maps
// TODO find a better place for this
importedModules.set(
'preact',
`https://esm.sh/preact@${config.preactVersion}`
);
importedModules.set(
'preact/',
`https://esm.sh/preact@${config.preactVersion}/`
);

const component = {
...components[componentId],
componentId,
componentSource,
moduleImports: importedModules,
};

if (!rootComponentSource && componentId === rootComponentPath) {
setRootComponentSource(componentId);
}
Expand All @@ -269,6 +294,7 @@ export function useWebEngine({
isValidRootComponentPath,
flags?.bosLoaderUrl,
addSource,
config?.preactVersion,
]);

return {
Expand Down
177 changes: 87 additions & 90 deletions packages/compiler/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,74 +4,43 @@ import {
buildComponentFunction,
buildComponentFunctionName,
} from './component';
import {
buildComponentImportStatements,
buildModuleImports,
buildModulePackageUrl,
extractImportStatements,
} from './import';
import { parseChildComponents, ParsedChildComponent } from './parser';
import { fetchComponentSources } from './source';
import { transpileSource } from './transpile';

export type ComponentCompilerRequest =
| CompilerExecuteAction
| CompilerInitAction;

interface CompilerExecuteAction {
action: 'execute';
componentId: string;
}

interface CompilerInitAction {
action: 'init';
localFetchUrl?: string;
}

export interface ComponentCompilerResponse {
componentId: string;
componentSource: string;
rawSource: string;
componentPath: string;
error?: Error;
}

type SendMessageCallback = (res: ComponentCompilerResponse) => void;

interface ComponentCompilerParams {
sendMessage: SendMessageCallback;
}

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

interface ParseComponentTreeParams {
mapped: { [key: string]: { transpiled: string } };
transpiledComponent: string;
componentPath: string;
isComponentPathTrusted?: (path: string) => boolean;
trustedRoot?: TrustedRoot;
}

interface TrustedRoot {
rootPath: string;
trustMode: string;
/* predicates for determining trust under a trusted root */
matchesRootAuthor: (path: string) => boolean;
}
import type {
CompilerExecuteAction,
CompilerInitAction,
ComponentCompilerParams,
ComponentTreeNode,
ParseComponentTreeParams,
SendMessageCallback,
TranspiledComponentLookupParams,
TrustedRoot,
} from './types';

export class ComponentCompiler {
private bosSourceCache: Map<string, Promise<string>>;
private compiledSourceCache: Map<string, string | null>;
private readonly sendWorkerMessage: SendMessageCallback;
private hasFetchedLocal: boolean = false;
private localFetchUrl?: string;
private preactVersion?: string;

constructor({ sendMessage }: ComponentCompilerParams) {
this.bosSourceCache = new Map<string, Promise<string>>();
this.compiledSourceCache = new Map<string, string>();
this.sendWorkerMessage = sendMessage;
}

init({ localFetchUrl }: CompilerInitAction) {
init({ localFetchUrl, preactVersion }: CompilerInitAction) {
this.localFetchUrl = localFetchUrl;
this.preactVersion = preactVersion;
}

getComponentSources(componentPaths: string[]) {
Expand Down Expand Up @@ -147,20 +116,55 @@ export class ComponentCompiler {
return false;
}

/**
* Traverse the Component tree, building the set of Components to be included within the container
* @param componentPath the path to the root Component of the current tree
* @param transpiledComponent transpiled JSX source code
* @param components set of Components accumulated while traversing the Component tree
* @param isComponentPathTrusted callback to determine whether the current Component is to be trusted in the container
* @param isRoot flag indicating whether the current Component is the container root
* @param trustedRoot the trust mode inherited by the current Component from an ancestor Component (e.g. that extends trust to all child Components of the same author)
*/
async parseComponentTree({
componentPath,
transpiledComponent,
mapped,
componentSource,
components,
isComponentPathTrusted,
isRoot,
trustedRoot,
}: ParseComponentTreeParams) {
// separate out import statements from Component source
const { imports, source: cleanComponentSource } =
extractImportStatements(componentSource);

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

// wrap the Component's JSX body source in a function to be rendered as a Component
const componentFunctionSource = buildComponentFunction({
componentPath,
componentSource: cleanComponentSource,
componentImports,
isRoot,
});

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

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

// 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
const buildTrustedRootKey = ({ index, path }: ParsedChildComponent) =>
`${index}:${path}`;

const trustedRoots = childComponents.reduce((trusted, childComponent) => {
const { trustMode } = childComponent;

Expand All @@ -178,53 +182,45 @@ export class ComponentCompiler {
return trusted;
}, new Map<string, TrustedRoot>());

// get the set of trusted child Components to be inlined in the container
const trustedChildComponents = childComponents.filter((child) =>
ComponentCompiler.isChildComponentTrusted(child, isComponentPathTrusted)
);

// add the transformed source to the returned Component tree
mapped[componentPath] = {
components.set(componentPath, {
imports,
// replace each child [Component] reference in the target Component source
// with the generated name of the inlined Component function definition
transpiled: trustedChildComponents.reduce(
(transformed, { path, transform }) =>
transform(transformed, buildComponentFunctionName(path)),
transpiledComponent
),
};
});

// fetch the set of child Component sources not already added to the tree
const childComponentSources = this.getComponentSources(
trustedChildComponents
.map(({ path }) => path)
.filter((path) => !(path in mapped))
.filter((path) => !(path in components))
);

// transpile the set of new child Components and recursively parse their Component subtrees
await Promise.all(
trustedChildComponents.map(async (childComponent) => {
const { path } = childComponent;
let transpiledChild = mapped[path]?.transpiled;
if (!transpiledChild) {
transpiledChild = this.getTranspiledComponentSource({
componentPath: path,
componentSource: buildComponentFunction({
componentPath: path,
componentSource: (await childComponentSources.get(path))!,
isRoot: false,
}),
isRoot: false,
});
}
const componentSource = (await childComponentSources.get(path))!;

const childTrustedRoot =
trustedRoots.get(buildTrustedRootKey(childComponent)) || trustedRoot;

await this.parseComponentTree({
componentPath: path,
transpiledComponent: transpiledChild,
mapped,
componentSource,
components,
trustedRoot: childTrustedRoot,
isRoot: false,
isComponentPathTrusted:
trustedRoot?.trustMode === TrustMode.Sandboxed
? undefined
Expand All @@ -238,7 +234,7 @@ export class ComponentCompiler {
})
);

return mapped;
return components;
}

async compileComponent({ componentId }: CompilerExecuteAction) {
Expand All @@ -259,37 +255,38 @@ export class ComponentCompiler {
throw new Error(`Component not found at ${componentPath}`);
}

const componentFunctionSource = buildComponentFunction({
componentPath,
componentSource: source,
isRoot: true,
});
const transpiledComponent = this.getTranspiledComponentSource({
componentPath,
componentSource: componentFunctionSource,
isRoot: true,
});

let componentSource = transpiledComponent;
// recursively parse the Component tree for child Components
const transformedComponents = await this.parseComponentTree({
componentPath,
transpiledComponent,
mapped: {},
componentSource: source,
components: new Map<string, ComponentTreeNode>(),
isRoot: true,
});

const [rootComponent, ...childComponents] = Object.values(
transformedComponents
).map(({ transpiled }) => transpiled);
const aggregatedSourceLines = rootComponent.split('\n');
aggregatedSourceLines.splice(1, 0, childComponents.join('\n\n'));
componentSource = aggregatedSourceLines.join('\n');
const containerModuleImports = [...transformedComponents.values()]
.map(({ imports }) => imports)
.flat();
const importStatements = buildModuleImports(containerModuleImports);
const importedModules = [
...new Set(containerModuleImports.map(({ module }) => module)),
].reduce((modules, module) => {
modules.set(module, buildModulePackageUrl(module, this.preactVersion!));
return modules;
}, new Map<string, string>());

const componentSource = [
...importStatements,
...[...transformedComponents.values()].map(
({ transpiled }) => transpiled
),
].join('\n\n');

this.sendWorkerMessage({
componentId,
componentSource,
rawSource: source,
componentPath,
importedModules,
});
}

Expand Down
Loading
Loading