Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/flow-remote' into flow-remote
Browse files Browse the repository at this point in the history
  • Loading branch information
ins0 committed Jan 6, 2025
2 parents 69bbd82 + 5e2ee17 commit ba97edc
Show file tree
Hide file tree
Showing 252 changed files with 19,388 additions and 609 deletions.
110 changes: 95 additions & 15 deletions .pnp.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion demos/remote-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"build": "run next build",
"build:deps:watch": "run -T build:deps:watch @mittwald/flow-demo-remote-dom",
"build:deps:watch": "nx watch -d -p \"@mittwald/flow-demo-remote-dom\" -- nx run-many -t build -p \\$NX_PROJECT_NAME --exclude=\"@mittwald/flow-demo-remote-dom\"",
"dev": "run nx run-many --outputStyle=stream --projects=@mittwald/flow-demo-remote-dom --targets=start,build:deps:watch",
"start": "run next dev",
"test:compile": "run tsc --noEmit"
Expand Down
16 changes: 16 additions & 0 deletions dev/remote-components-generator/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { RemoteComponentGeneratorConfig } from "./types/config";

export const remoteComponentGeneratorConfig: RemoteComponentGeneratorConfig = {
components: {
ActionStateContext: {
ignore: true,
},
Form: {
ignore: true,
},
List: {
ignore: true,
},
},
ignoreProps: ["tunnelId", "ref", "key"],
};
106 changes: 106 additions & 0 deletions dev/remote-components-generator/generateRemoteComponents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { ComponentFileLoader } from "./loading/ComponentFileLoader";
import { ComponentFileContentLoader } from "./loading/ComponentFileContentLoader";
import {
generateRemoteReactComponentFile,
generateRemoteReactComponentIndexFile,
} from "./generation/generateRemoteReactComponentFile";
import jetpack from "fs-jetpack";
import { prepareTypeScriptOutput } from "./generation/prepareTypeScriptOutput";
import { generateRemoteElementFile } from "./generation/generateRemoteElementFile";
import { remoteComponentGeneratorConfig } from "./config";
import type { RemoteComponentGeneratorConfig } from "./types/config";

const componentFileLoader = new ComponentFileLoader();
const componentFileContentLoader = new ComponentFileContentLoader(
componentFileLoader,
);

async function generate() {
const config: RemoteComponentGeneratorConfig = remoteComponentGeneratorConfig;

console.log("🤓 Read component specification file");
const componentSpecificationFile = await componentFileLoader.loadFile();
console.log("✅ Done");
console.log("");

console.log("🧐 Parse component specification file");
let components = await componentFileContentLoader.parseJson(
componentSpecificationFile,
);
console.log("✅ Done");
console.log("");

console.log("💣 Remove ignored components and props");
for (const [componentName, componentConfig] of Object.entries(
config.components,
)) {
if (componentConfig.ignore) {
console.log(` .. removing "${componentName}"`);
components = components.filter(
(item) => item.displayName != componentName,
);
}
if (componentConfig.ignoreProps) {
const component = components.find(
(item) => item.displayName == componentName,
);
if (component?.props) {
componentConfig.ignoreProps.map((ignoredProp) => {
console.log(` .. removing ${componentName}'s "${ignoredProp}" prop`);
delete component.props[ignoredProp];
});
}
}
}
console.log("✅ Done");
console.log("");

{
console.log("📝️ Generating remote-react-component files");

const dir = `packages/remote-react-components/src/auto-generated`;
jetpack.remove(dir);

for (const component of components) {
const remoteReactComponentFile =
generateRemoteReactComponentFile(component);
await jetpack.writeAsync(
`${dir}/${component.displayName}.ts`,
await prepareTypeScriptOutput(remoteReactComponentFile),
);
}
const indexFile = generateRemoteReactComponentIndexFile(components);
await jetpack.writeAsync(
`${dir}/index.ts`,
await prepareTypeScriptOutput(indexFile),
);
console.log("✅ Done");
console.log("");
}

{
console.log("📝️ Generating remote-element files");

const dir = `packages/remote-elements/src/auto-generated`;
const indexFile = generateRemoteReactComponentIndexFile(components);

jetpack.remove(dir);

for (const component of components) {
const remoteElementFile = generateRemoteElementFile(component);
await jetpack.writeAsync(
`${dir}/${component.displayName}.ts`,
await prepareTypeScriptOutput(remoteElementFile),
);
}
await jetpack.writeAsync(
`${dir}/index.ts`,
await prepareTypeScriptOutput(indexFile),
);
console.log("✅ Done");
console.log("");
console.log("✅ Generation finished successfully");
}
}

void generate();
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { ComponentDoc } from "react-docgen-typescript";
import { kebabize } from "../lib/kebabize";
import { remoteComponentGeneratorConfig } from "../config";

export function generateRemoteElementFile(
componentSpecification: ComponentDoc,
) {
const config = remoteComponentGeneratorConfig;
const componentProps = componentSpecification.props;

config.ignoreProps.map((prop) => delete componentProps[prop]);

const t = {
element: `Remote${componentSpecification.displayName}Element`,
propsType: `${componentSpecification.displayName}Props`,
name: componentSpecification.displayName,
props: Object.keys(componentProps)
.filter((propName) => !propName.startsWith("on"))
.map((propName) => {
const key = propName.includes("-") ? `'${propName}'` : propName;
return `${key}: {}`;
})
.join(",\n"),
events: Object.keys(componentProps)
.filter((propName) => propName.startsWith("on"))
.map((propName) => {
const formattedName = propName[2].toLowerCase() + propName.slice(3);
return `${formattedName}: {}`;
})
.join(",\n"),
};

return `\
import { FlowRemoteElement } from "@/lib/FlowRemoteElement";
import type { ${t.propsType} } from "@mittwald/flow-react-components/${t.name}";
export type { ${t.propsType} } from "@mittwald/flow-react-components/${t.name}";
export class ${t.element} extends FlowRemoteElement<${t.propsType}> {
static get remoteProperties() {
return {
${t.props}
};
}
static get remoteEvents() {
return {
${t.events}
};
}
}
declare global {
interface HTMLElementTagNameMap {
"flr-${kebabize(t.name)}": InstanceType<typeof ${t.element}>;
}
}
customElements.define("flr-${kebabize(t.name)}", ${t.element});
`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { ComponentDoc } from "react-docgen-typescript";
import { kebabize } from "../lib/kebabize";
import { remoteComponentGeneratorConfig } from "../config";

export function generateRemoteReactComponentFile(
componentSpecification: ComponentDoc,
) {
const config = remoteComponentGeneratorConfig;
const componentProps = componentSpecification.props;

config.ignoreProps.map((prop) => delete componentProps[prop]);

const t = {
component: `Remote${componentSpecification.displayName}Element`,
name: componentSpecification.displayName,
events: Object.keys(componentProps)
.filter((propName) => propName.startsWith("on"))
.map((propName) => {
const formattedName = propName[2].toLowerCase() + propName.slice(3);
return `${propName}: { event: "${formattedName}" } as never`;
})
.join(",\n"),
};

return `\
import createFlowRemoteComponent from "@/lib/createFlowRemoteComponent";
import { ${t.component} } from "@mittwald/flow-remote-elements";
export const ${t.name} = createFlowRemoteComponent("flr-${kebabize(t.name)}", "${t.name}", ${t.component}, {${
t.events && t.events.length > 0
? `eventProps: {
${t.events}
},`
: ""
}});
`;
}

export function generateRemoteReactComponentIndexFile(
componentSpecifications: ComponentDoc[],
) {
let indexFile = "";

componentSpecifications.map((component) => {
indexFile += `export * from "./${component.displayName}";`;
});

return indexFile;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { format } from "../lib/format";

const header = `\
/* eslint-disable */
/* prettier-ignore */
/* This file is auto-generated with the remote-components-generator */
`;

export const prepareTypeScriptOutput = async (
content: string,
): Promise<string> => {
const formatted = await format(content);
return header + formatted;
};
20 changes: 20 additions & 0 deletions dev/remote-components-generator/lib/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import prettier from "prettier";
import VError from "verror";
import { makeError } from "./makeError";

export const format = async (ts: string): Promise<string> => {
try {
return await prettier.format(ts, {
plugins: [],
parser: "typescript",
});
} catch (error) {
throw new VError(
{
cause: makeError(error),
name: "CodeFormattingError",
},
"Failed to format the generated code. This usually happens, when the generated code has syntax errors. Please file an issue.",
);
}
};
5 changes: 5 additions & 0 deletions dev/remote-components-generator/lib/kebabize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const kebabize = (str: string): string =>
str.replace(
/[A-Z]+(?![a-z])|[A-Z]/g,
($, ofs) => (ofs ? "-" : "") + $.toLowerCase(),
);
12 changes: 12 additions & 0 deletions dev/remote-components-generator/lib/makeError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import VError from "verror";
import { getProperty } from "dot-prop";

export const makeError = (error: unknown): Error =>
error instanceof Error
? error
: new VError(
{
name: getProperty(error, "name") ?? "Error",
},
getProperty(error, "message") ?? "",
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { parseAsync } from "yieldable-json";
import VError from "verror";
import { makeError } from "../lib/makeError";
import type { FileContentLoader, FileLoader } from "./types";
import type { ComponentDoc } from "react-docgen-typescript";

export class ComponentFileContentLoader implements FileContentLoader {
public readonly fileLoader: FileLoader;

public constructor(fileLoader: FileLoader) {
this.fileLoader = fileLoader;
}

public async load() {
try {
const fileContent = await this.fileLoader.loadFile();
return await this.parseJson(fileContent);
} catch (error) {
throw new VError(
{
cause: makeError(error),
name: "ComponentFileContentLoaderError",
},
"Failed loading content",
);
}
}

public async parseJson(json: string): Promise<ComponentDoc[]> {
return new Promise((res, rej) => {
return parseAsync(json, (err: Error | null, data: unknown) => {
if (err) {
rej(err);
} else {
res(data as ComponentDoc[]);
}
});
});
}
}
26 changes: 26 additions & 0 deletions dev/remote-components-generator/loading/ComponentFileLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import jetpack from "fs-jetpack";
import VError from "verror";
import { makeError } from "../lib/makeError";
import type { FileLoader } from "./types";

export class ComponentFileLoader implements FileLoader {
public async loadFile(): Promise<string> {
try {
const file = await jetpack.readAsync(
"./packages/components/out/doc-properties.json",
);
if (file === undefined || file === "") {
throw new Error(`doc-properties.json file not found`);
}
return file;
} catch (error) {
throw new VError(
{
cause: makeError(error),
name: "ComponentFileLoaderError",
},
"File loading failed",
);
}
}
}
9 changes: 9 additions & 0 deletions dev/remote-components-generator/loading/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ComponentDoc } from "react-docgen-typescript";

export interface FileLoader {
loadFile(): Promise<string>;
}

export interface FileContentLoader {
parseJson(json: string): Promise<ComponentDoc[]>;
}
9 changes: 9 additions & 0 deletions dev/remote-components-generator/types/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
interface RemoteComponentGeneratorConfigComponent {
ignore?: boolean;
ignoreProps?: string[];
}

export interface RemoteComponentGeneratorConfig {
components: Record<string, RemoteComponentGeneratorConfigComponent>;
ignoreProps: string[];
}
Loading

0 comments on commit ba97edc

Please sign in to comment.