Skip to content

Commit

Permalink
Harmony/scope (#2861)
Browse files Browse the repository at this point in the history
* component history working

* builder api working

* fixed component model when for new components

* prettier2 execution

* made workspace work as UI root and refactored few bad dependency relationships (e.g. bundler) to the workspace

* scope is rendering

* scope sidebar is rendering

* workspace and scope are now both fetching from component host

* lint errors
  • Loading branch information
ranm8 committed Jul 19, 2020
1 parent 030eeb7 commit c064f84
Show file tree
Hide file tree
Showing 76 changed files with 837 additions and 221 deletions.
22 changes: 10 additions & 12 deletions src/extensions/bundler/bundler.extension.ts
@@ -1,25 +1,22 @@
import { Slot, SlotRegistry } from '@teambit/harmony';
import { Component } from '../component';
import { WorkspaceExt, Workspace } from '../workspace';
import { Component, ComponentExtension } from '../component';
import { DevServerService } from './dev-server.service';
import { Environments } from '../environments';
import { GraphQLExtension } from '../graphql';
import { devServerSchema } from './dev-server.graphql';
import { ComponentServer } from './component-server';
import { BrowserRuntime } from './browser-runtime';
import { UIRoot } from '../ui';

export type BrowserRuntimeSlot = SlotRegistry<BrowserRuntime>;

/**
* bundler extension.
*/
export class BundlerExtension {
constructor(
/**
* workspace extension.
*/
private workspace: Workspace,
static id = '@teambit/bundler';

constructor(
/**
* environments extension.
*/
Expand All @@ -40,8 +37,9 @@ export class BundlerExtension {
* load all given components in corresponding dev servers.
* @param components defaults to all components in the workspace.
*/
async devServer(components?: Component[]) {
const envRuntime = await this.envs.createEnvironment(components || (await this.workspace.list()));
async devServer(components: Component[], root: UIRoot) {
const envRuntime = await this.envs.createEnvironment(components);
this.devService.uiRoot = root;
const executionResponse = await envRuntime.run(this.devService);

this._componentServers = executionResponse.map((res) => res.res);
Expand Down Expand Up @@ -86,14 +84,14 @@ export class BundlerExtension {

static slots = [Slot.withType<BrowserRuntime>()];

static dependencies = [WorkspaceExt, Environments, GraphQLExtension];
static dependencies = [Environments, GraphQLExtension, ComponentExtension];

static async provider(
[workspace, envs, graphql]: [Workspace, Environments, GraphQLExtension],
[envs, graphql]: [Environments, GraphQLExtension],
config,
[runtimeSlot]: [BrowserRuntimeSlot]
) {
const bundler = new BundlerExtension(workspace, envs, new DevServerService(runtimeSlot, workspace), runtimeSlot);
const bundler = new BundlerExtension(envs, new DevServerService(runtimeSlot), runtimeSlot);
graphql.register(devServerSchema(bundler));
return bundler;
}
Expand Down
3 changes: 2 additions & 1 deletion src/extensions/bundler/dev-server.graphql.ts
@@ -1,10 +1,11 @@
import gql from 'graphql-tag';
import { Schema } from '../graphql';
import { BundlerExtension } from './bundler.extension';
import { Component } from '../component';

export function devServerSchema(bundler: BundlerExtension): Schema {
return {
typeDefs: `
typeDefs: gql`
extend type Component {
server: ComponentServer
}
Expand Down
16 changes: 11 additions & 5 deletions src/extensions/bundler/dev-server.service.ts
Expand Up @@ -8,7 +8,7 @@ import { ComponentServer } from './component-server';
import { BindError } from './exceptions';
import { BrowserRuntimeSlot } from './bundler.extension';
import { DevServerContext } from './dev-server-context';
import { Workspace } from '../workspace';
import { UIRoot } from '../ui';

export class DevServerService implements EnvService {
constructor(
Expand All @@ -18,11 +18,15 @@ export class DevServerService implements EnvService {
private runtimeSlot: BrowserRuntimeSlot,

/**
* workspace extension.
* main path of the dev server to execute on.
*/
private workspace: Workspace
private _uiRoot?: UIRoot
) {}

set uiRoot(value: UIRoot) {
this._uiRoot = value;
}

async run(context: ExecutionContext) {
const devServer: DevServer = context.env.getDevServer(await this.buildContext(context));
const port = await selectPort();
Expand All @@ -47,12 +51,14 @@ export class DevServerService implements EnvService {
* computes the bundler entry.
*/
private async getEntry(context: ExecutionContext): Promise<string[]> {
const uiRoot = this._uiRoot;
if (!uiRoot) throw new Error('a root must be provided by UI root');
const mainFiles = context.components.map((component) => {
const path = join(
// :TODO check how it works with david. Feels like a side-effect.
// this.workspace.componentDir(component.id, {}, { relative: true }),
// @ts-ignore
// component.state._consumer.componentMap?.getComponentDir()
this.workspace.componentDir(component.id, {}, { relative: true }),
uiRoot.componentDir(component.id, {}, { relative: true }),
// @ts-ignore
component.config.main
);
Expand Down
2 changes: 2 additions & 0 deletions src/extensions/changelog/changelog.ui.tsx
Expand Up @@ -22,3 +22,5 @@ export class ChangeLogUI {
return ui;
}
}

export default ChangeLogUI;
29 changes: 29 additions & 0 deletions src/extensions/component/component-meta.ts
@@ -0,0 +1,29 @@
import { ComponentID } from './id';
import { capitalize } from '../utils/capitalize';

export class ComponentMeta {
constructor(
/**
* id the component.
*/
readonly id: ComponentID
) {}

/**
* display name of the component.
*/
get displayName() {
const tokens = this.id.name.split('-').map((token) => capitalize(token));
return tokens.join(' ');
}

toObject() {
return {
id: this.id.toObject(),
};
}

static from(object: { [key: string]: any }) {
return new ComponentMeta(ComponentID.fromObject(object.id));
}
}
33 changes: 28 additions & 5 deletions src/extensions/component/component.extension.ts
@@ -1,21 +1,44 @@
/* eslint-disable max-classes-per-file */
import { Slot } from '@teambit/harmony';
import { Slot, SlotRegistry } from '@teambit/harmony';
import { GraphQLExtension } from '../graphql';
import { componentSchema } from './component.graphql';
import { ComponentFactory } from './component-factory';
import { HostNotFound } from './exceptions';

export type ConfigFunc = () => any;
export type ComponentHostSlot = SlotRegistry<ComponentFactory>;

export class ComponentExtension {
static id = '@teambit/component';

constructor(
/**
* slot for component hosts to register.
*/
private hostSlot: ComponentHostSlot
) {}

/**
* register a new component host.
*/
registerHost(host: ComponentFactory) {
this.hostSlot.register(host);
return this;
}

getHost(id: string): ComponentFactory {
const host = this.hostSlot.get(id);
if (!host) throw new HostNotFound();
return host;
}

static slots = [Slot.withType<ComponentFactory>()];

static dependencies = [GraphQLExtension];

static async provider([graphql]: [GraphQLExtension]) {
graphql.register(componentSchema());
return new ComponentExtension();
static async provider([graphql]: [GraphQLExtension], config, [hostSlot]: [ComponentHostSlot]) {
const componentExtension = new ComponentExtension(hostSlot);
graphql.register(componentSchema(componentExtension));
return componentExtension;
}
}

Expand Down
30 changes: 26 additions & 4 deletions src/extensions/component/component.graphql.ts
@@ -1,8 +1,11 @@
import gql from 'graphql-tag';
import { Component } from './component';
import { ComponentMeta } from './component-meta';
import componentIdToPackageName from '../../utils/bit/component-id-to-package-name';
import { ComponentExtension } from './component.extension';
import { ComponentFactory } from './component-factory';

export function componentSchema() {
export function componentSchema(componentExtension: ComponentExtension) {
return {
typeDefs: gql`
type ComponentID {
Expand Down Expand Up @@ -79,14 +82,22 @@ export function componentSchema() {
id: ComponentID
displayName: String
}
type ComponentHost {
get(id: String!): Component
}
type Query {
getHost(id: String): ComponentHost
}
`,
resolvers: {
ComponentMeta: {
id: (component: Component) => component.id._legacy.serialize(),
displayName: (component: Component) => component.displayName,
id: (component: ComponentMeta) => component.id.toObject(),
displayName: (component: ComponentMeta) => component.displayName,
},
Component: {
id: (component: Component) => component.id._legacy.serialize(),
id: (component: Component) => component.id.toObject(),
displayName: (component: Component) => component.displayName,
headTag: (component: Component) => component.headTag,
tags: (component) => {
Expand All @@ -109,6 +120,17 @@ export function componentSchema() {
});
},
},
ComponentHost: {
get: async (host: ComponentFactory, { id }: { id: string }) => {
const componentId = await host.resolveComponentId(id);
return host.get(componentId);
},
},
Query: {
getHost: (componentExt: ComponentExtension, { id }: { id: string }) => {
return componentExtension.getHost(id);
},
},
},
};
}
27 changes: 9 additions & 18 deletions src/extensions/component/component.ui.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import { RouteProps, NavLinkProps } from 'react-router-dom';
import { Slot } from '@teambit/harmony';
import { WorkspaceUI } from '../workspace/workspace.ui';
import { Component } from './ui/component';
import { RouteSlot, NavigationSlot } from '../react-router/slot-router';

Expand All @@ -18,20 +17,15 @@ export type MenuItem = {
label: JSX.Element | string | null;
};

const componentIdUrlRegex = '[\\w\\/-]*[\\w-]';
export const componentIdUrlRegex = '[\\w\\/-]*[\\w-]';

export class ComponentUI {
constructor(private routeSlot: RouteSlot, private navSlot: NavigationSlot, private widgetSlot: NavigationSlot) {}

/**
* expose the route for a component.
*/
get componentRoute() {
return {
// trailing slash to avoid including '/' in componentId
path: `/:componentId(${componentIdUrlRegex})/`,
children: <Component navSlot={this.navSlot} routeSlot={this.routeSlot} widgetSlot={this.widgetSlot} />,
};
readonly routePath = `/:componentId(${componentIdUrlRegex})`;

getComponentUI(host: string) {
return <Component navSlot={this.navSlot} routeSlot={this.routeSlot} widgetSlot={this.widgetSlot} host={host} />;
}

registerRoute(route: RouteProps) {
Expand All @@ -47,17 +41,14 @@ export class ComponentUI {
this.widgetSlot.register(widget);
}

static dependencies = [WorkspaceUI];
static dependencies = [];

static slots = [Slot.withType<RouteProps>(), Slot.withType<NavigationSlot>(), Slot.withType<NavigationSlot>()];

static async provider(
[workspace]: [WorkspaceUI],
config,
[routeSlot, navSlot, widgetSlot]: [RouteSlot, NavigationSlot, NavigationSlot]
) {
static async provider(deps, config, [routeSlot, navSlot, widgetSlot]: [RouteSlot, NavigationSlot, NavigationSlot]) {
const componentUI = new ComponentUI(routeSlot, navSlot, widgetSlot);
workspace.registerRoute(componentUI.componentRoute);
return componentUI;
}
}

export default ComponentUI;
5 changes: 5 additions & 0 deletions src/extensions/component/exceptions/host-not-found.ts
@@ -0,0 +1,5 @@
export class HostNotFound extends Error {
toString() {
return `[component] error: host not found`;
}
}
1 change: 1 addition & 0 deletions src/extensions/component/exceptions/index.ts
@@ -1,2 +1,3 @@
// eslint-disable-next-line import/prefer-default-export
export { default as NothingToSnap } from './nothing-to-snap';
export { HostNotFound } from './host-not-found';
5 changes: 3 additions & 2 deletions src/extensions/component/ui/component.tsx
Expand Up @@ -11,17 +11,18 @@ export type ComponentProps = {
navSlot: NavigationSlot;
routeSlot: RouteSlot;
widgetSlot: NavigationSlot;
host: string;
};

/**
* main UI component of the Component extension.
*/
export function Component({ navSlot, routeSlot, widgetSlot }: ComponentProps) {
export function Component({ navSlot, routeSlot, widgetSlot, host }: ComponentProps) {
const {
params: { componentId },
} = useRouteMatch();

const component = useComponentQuery(componentId);
const component = useComponentQuery(componentId, host);
if (!component) return <FullLoader />;

return (
Expand Down
23 changes: 7 additions & 16 deletions src/extensions/component/ui/use-component-query.ts
@@ -1,14 +1,12 @@
import { useMemo } from 'react';
import { gql } from 'apollo-boost';

import { ComponentModel } from './component-model';
import { useDataQuery } from '../../ui/ui/data/use-data-query';
import { ComponentModelProps } from './component-model/component-model';

const GET_COMPONENT = gql`
query Component($id: String!) {
workspace {
getComponent(id: $id) {
query Component($id: String!, $extensionId: String!) {
getHost(id: $extensionId) {
get(id: $id) {
id {
name
version
Expand All @@ -35,20 +33,13 @@ const GET_COMPONENT = gql`
}
`;

// this is not ideal. can we derive type from gql?
type ComponentQueryData = {
workspace?: {
getComponent?: ComponentModelProps;
};
};

/** provides data to component ui page, making sure both variables and return value are safely typed and memoized */
export function useComponentQuery(componentId: string) {
const { data } = useDataQuery<ComponentQueryData>(GET_COMPONENT, {
variables: { id: componentId },
export function useComponentQuery(componentId: string, host: string) {
const { data } = useDataQuery(GET_COMPONENT, {
variables: { id: componentId, extensionId: host },
});

const rawComponent = data?.workspace?.getComponent;
const rawComponent = data?.getHost?.get;

return useMemo(() => (rawComponent ? ComponentModel.from(rawComponent) : undefined), [rawComponent]);
}
2 changes: 1 addition & 1 deletion src/extensions/compositions/compositions.tsx
Expand Up @@ -40,7 +40,7 @@ export function Compositions() {
const properties = R.path(['workspace', 'getDocs', 'properties'], data);

// reset selected composition when component changes.
// this does trigger rerender, but perf seems to be ok
// this does trigger renderer, but perf seems to be ok
useEffect(() => {
selectComposition(component.compositions[0]);
}, [component]);
Expand Down

0 comments on commit c064f84

Please sign in to comment.