Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,7 @@ test-results/
playwright-report/
blob-report/
playwright/.cache/

# Web components metadata
packages/web-components/components-metadata.json
packages/web-components/custom-elements.json
3 changes: 1 addition & 2 deletions docs/components/[component].paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ export default {

const docsDir = path.resolve(dirname, "..");
const workspaceDir = path.join(docsDir, "..");
const distDir = path.join(workspaceDir, "dist");
const metadataFile = path.join(distDir, "components-metadata.json");
const metadataFile = path.join(workspaceDir, "packages/web-components/components-metadata.json");

const metadata = JSON.parse(
fs.readFileSync(metadataFile).toString(),
Expand Down
3 changes: 1 addition & 2 deletions docs/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { type Metadata } from "../types/docs.ts";
const { dirname } = getFileMeta(import.meta.url);

const workspaceDir = path.join(dirname, "..");
const distDir = path.join(workspaceDir, "dist");
const metadataFile = path.join(distDir, "components-metadata.json");
const metadataFile = path.join(workspaceDir, "packages/web-components/components-metadata.json");

const getSidebarItems = (): DefaultTheme.Sidebar => {
return (
Expand Down
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,10 @@
"docs:vitepress:dev": "pnpm --filter docs run dev",
"docs:vitepress:build": "pnpm --filter docs run build",
"docs:vitepress:preview": "pnpm --filter docs run preview",
"docs:dev": "run-s analyze docs:gen:metadata docs:vitepress:dev",
"docs:build": "run-s analyze docs:gen:metadata docs:vitepress:build",
"docs:dev": "run-s gen:metadata docs:vitepress:dev",
"docs:build": "run-s gen:metadata docs:vitepress:build",
"docs:preview": "run-s docs:vitepress:preview",
"docs:gen:metadata": "tsx ./scripts/generate-docs-metadata.ts",
"analyze": "tsx ./scripts/generate-cem.ts"
"gen:metadata": "pnpm --filter @tapsioss/web-components run gen:metadata"
},
"devDependencies": {
"@custom-elements-manifest/analyzer": "^0.10.2",
Expand Down
168 changes: 66 additions & 102 deletions packages/react-components/scripts/generate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable no-console */
import type { Package } from "custom-elements-manifest";
import Mustache from "mustache";
import { exec } from "node:child_process";
import * as fs from "node:fs";
Expand All @@ -10,145 +9,116 @@ import { chain } from "stream-chain";
import Parser from "stream-json/Parser";
import StreamValues from "stream-json/streamers/StreamValues";
import { fileExists, getFileMeta } from "../../../scripts/utils.ts";
import { type Component, type Metadata } from "../../../types/docs.ts";

const asyncExec = promisify(exec);

const COMPONENT_NAMESPACE = "ComponentNamespace";
const LIT_REACT_NAMESPACE = "LitReact";

const { dirname } = getFileMeta(import.meta.url);

const packageDir = path.resolve(dirname, "..");
const workspaceDir = path.resolve(packageDir, "../../");
const srcDir = path.join(packageDir, "src");
const templatesDir = path.join(packageDir, "templates");
const webComponentsSrcDir = path.join(
workspaceDir,
"packages/web-components/src",
);

const barrelFilePath = path.join(srcDir, "index.ts");
const cemGeneratorPath = path.join(workspaceDir, "scripts/generate-cem.ts");

const componentTemplatePath = path.join(templatesDir, "component.txt");
const cemPath = path.join(workspaceDir, "dist/custom-elements.json");
const metadataPath = path.resolve(packageDir, "../web-components/components-metadata.json");

const START_COMMENT = "/* START: AUTO-GENERATED [DO_NOT_REMOVE] */";
const END_COMMENT = "/* END: AUTO-GENERATED [DO_NOT_REMOVE] */";

const generateImports = (
elementClass: string,
moduleResolutionRegistryPath: string,
) => {
const getComponentImports = (component: Component) => {
return [
`import { createComponent } from "@lit/react";`,
`import * as ${LIT_REACT_NAMESPACE} from "@lit/react";`,
`import * as React from "react";`,
`import { ${elementClass} } from "${moduleResolutionRegistryPath}";`,
`import * as ${COMPONENT_NAMESPACE} from "${component.importPaths.webComponents}";`,
].join("\n");
};

const getComponentCode = async (component: Component): Promise<string> => {
const componentTemplateStr = await fs.promises.readFile(
componentTemplatePath,
{ encoding: "utf-8" },
);

const events = `{ ${
component.events
?.map(event => {
const eventName = event.name;
const eventClass = event.type.text;

const eventNameInReact = [
`on`,
String(eventName).charAt(0).toUpperCase() +
String(eventName).slice(1),
].join("");

if (eventClass === "Event") {
return null;
}

return `${eventNameInReact}: '${eventName}' as ${LIT_REACT_NAMESPACE}.EventName<${COMPONENT_NAMESPACE}.${eventClass}>`;
})
.filter(event => event !== null)
.join(",") || ""
} }`;

return Mustache.render(
componentTemplateStr,
{
componentName: component.name.replace("Tapsi", ""),
elementTag: component.tagName,
elementClass: `${COMPONENT_NAMESPACE}.${component.name}`,
events,
},
{},
{ escape: v => v as string },
);
};

const generateExports = (componentName: string) => {
return [
`const ${componentName} = __${componentName};\n`,
`export default ${componentName};`,
].join("\n");
};

type ComponentData = Array<{
webComponentResolutionPath: string;
elementTag: string;
elementClassName: string;
componentName: string;
componentCode: string;
}>;

const transformToCEM = new Transform({
const transformToComponentsMetadata = new Transform({
objectMode: true,
transform(
chunk: {
key: PropertyKey;
value: Package;
value: Metadata;
},
_,
callback,
) {
callback(null, chunk.value);
},
});

const transformToComponentData = new Transform({
objectMode: true,
async transform(chunk: Package, _, callback) {
const streamData: ComponentData = [];

for (const module of chunk.modules) {
if (module.kind !== "javascript-module") continue;

const moduleSrc = module.path;
const moduleDir = path.dirname(moduleSrc);
const relativePath = path.relative(webComponentsSrcDir, moduleDir);

const ceDefinitions = (module.exports ?? []).filter(
ex => ex.kind === "custom-element-definition",
);

if (ceDefinitions.length === 0) {
console.error(`No "custom-element-definition" found for ${moduleSrc}.`);

continue;
}

const webComponentResolutionPath = `@tapsioss/web-components/${relativePath}`;

for (const ceDefinition of ceDefinitions) {
const elementTag = ceDefinition.name;
const elementClassName = ceDefinition.declaration.name;
const componentName = elementClassName.replace("Tapsi", "");

const componentTemplateStr = await fs.promises.readFile(
componentTemplatePath,
{ encoding: "utf-8" },
);

const componentCode = Mustache.render(
componentTemplateStr,
{
componentName,
elementTag,
elementClass: elementClassName,
events: JSON.stringify({}, null, 2),
},
{},
{ escape: v => v as string },
);

streamData.push({
webComponentResolutionPath,
elementTag,
elementClassName,
componentName,
componentCode,
});
}
}

callback(null, streamData);
callback(null, chunk.value.components);
},
});

const transformToComponentModule = new Transform({
objectMode: true,
async transform(chunk: ComponentData, _, callback) {
async transform(chunk: Component[], _, callback) {
for (const component of chunk) {
const {
componentCode,
componentName,
elementClassName,
webComponentResolutionPath,
} = component;
const componentName = component.name.replace("Tapsi", "");

const modulePath = path.join(srcDir, `${componentName}.ts`);
const moduleExists = fileExists(modulePath);

const componentCode = await getComponentCode(component);

if (moduleExists) {
const tempModulePath = path.join(srcDir, `${componentName}.temp.ts`);

const readModule = fs.createReadStream(modulePath, {
encoding: "utf-8",
autoClose: true,
});

const injectComponentCode = new Transform({
transform(chunk: Buffer, _, callback) {
const lines = chunk.toString("utf-8").split("\n");
Expand Down Expand Up @@ -178,11 +148,6 @@ const transformToComponentModule = new Transform({
},
});

const readModule = fs.createReadStream(modulePath, {
encoding: "utf-8",
autoClose: true,
});

const writeToTemp = fs.createWriteStream(tempModulePath, {
encoding: "utf-8",
flags: "w",
Expand All @@ -197,7 +162,7 @@ const transformToComponentModule = new Transform({
});
} else {
const moduleContent = [
generateImports(elementClassName, webComponentResolutionPath),
getComponentImports(component),
"\n",
START_COMMENT,
componentCode,
Expand Down Expand Up @@ -233,7 +198,7 @@ const transformToComponentModule = new Transform({
void (async () => {
console.time("generate");
const { stdout, stderr } = await asyncExec(
["tsx", cemGeneratorPath].join(" "),
"pnpm --filter @tapsioss/web-components run gen:metadata",
);

if (stderr) console.error(stderr);
Expand All @@ -243,16 +208,15 @@ void (async () => {

if (fileExists(barrelFilePath)) await fs.promises.rm(barrelFilePath);

fs.createReadStream(cemPath, {
fs.createReadStream(metadataPath, {
autoClose: true,
encoding: "utf-8",
})
.pipe(
chain([
Parser.make(),
StreamValues.make(),
transformToCEM,
transformToComponentData,
transformToComponentsMetadata,
transformToComponentModule,
] as const),
)
Expand Down
9 changes: 5 additions & 4 deletions packages/react-components/src/Avatar.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { createComponent } from "@lit/react";
import { TapsiAvatar } from "@tapsioss/web-components/avatar";
import * as LitReact from "@lit/react";
import * as ComponentNamespace from "@tapsioss/web-components/avatar";
import * as React from "react";

/* START: AUTO-GENERATED [DO_NOT_REMOVE] */
const __Avatar = createComponent({
const __Avatar = LitReact.createComponent({
tagName: "tapsi-avatar",
elementClass: TapsiAvatar,
elementClass: ComponentNamespace.TapsiAvatar,
react: React,
events: {},
});

/* END: AUTO-GENERATED [DO_NOT_REMOVE] */

const Avatar = __Avatar;
Expand Down
9 changes: 5 additions & 4 deletions packages/react-components/src/Badge.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { createComponent } from "@lit/react";
import { TapsiBadge } from "@tapsioss/web-components/badge";
import * as LitReact from "@lit/react";
import * as ComponentNamespace from "@tapsioss/web-components/badge";
import * as React from "react";

/* START: AUTO-GENERATED [DO_NOT_REMOVE] */
const __Badge = createComponent({
const __Badge = LitReact.createComponent({
tagName: "tapsi-badge",
elementClass: TapsiBadge,
elementClass: ComponentNamespace.TapsiBadge,
react: React,
events: {},
});

/* END: AUTO-GENERATED [DO_NOT_REMOVE] */

const Badge = __Badge;
Expand Down
9 changes: 5 additions & 4 deletions packages/react-components/src/BadgeWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { createComponent } from "@lit/react";
import { TapsiBadgeWrapper } from "@tapsioss/web-components/badge-wrapper";
import * as LitReact from "@lit/react";
import * as ComponentNamespace from "@tapsioss/web-components/badge-wrapper";
import * as React from "react";

/* START: AUTO-GENERATED [DO_NOT_REMOVE] */
const __BadgeWrapper = createComponent({
const __BadgeWrapper = LitReact.createComponent({
tagName: "tapsi-badge-wrapper",
elementClass: TapsiBadgeWrapper,
elementClass: ComponentNamespace.TapsiBadgeWrapper,
react: React,
events: {},
});

/* END: AUTO-GENERATED [DO_NOT_REMOVE] */

const BadgeWrapper = __BadgeWrapper;
Expand Down
9 changes: 5 additions & 4 deletions packages/react-components/src/Banner.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { createComponent } from "@lit/react";
import { TapsiBanner } from "@tapsioss/web-components/banner";
import * as LitReact from "@lit/react";
import * as ComponentNamespace from "@tapsioss/web-components/banner";
import * as React from "react";

/* START: AUTO-GENERATED [DO_NOT_REMOVE] */
const __Banner = createComponent({
const __Banner = LitReact.createComponent({
tagName: "tapsi-banner",
elementClass: TapsiBanner,
elementClass: ComponentNamespace.TapsiBanner,
react: React,
events: {},
});

/* END: AUTO-GENERATED [DO_NOT_REMOVE] */

const Banner = __Banner;
Expand Down
Loading