Skip to content

Commit ee76948

Browse files
authored
Merge pull request #1128 from Trendyol/next
v3.4.0
2 parents 16590a9 + 48698ed commit ee76948

File tree

77 files changed

+6201
-498
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+6201
-498
lines changed

.github/workflows/scorecard.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,14 @@ jobs:
4141
persist-credentials: false
4242

4343
- name: "Run analysis"
44-
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736
44+
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186
4545
with:
4646
results_file: results.sarif
4747
results_format: sarif
4848
publish_results: true
4949

5050
- name: "Upload artifact"
51-
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
51+
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
5252
with:
5353
name: SARIF file
5454
path: results.sarif

.github/workflows/security-gates.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ jobs:
1919
actions: read
2020
contents: read
2121
security-events: write
22+
secrets: inherit

cemPlugins/addJsDoc.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { parse } from "comment-parser";
2+
3+
/**
4+
* @returns {import('@custom-elements-manifest/analyzer').Plugin}
5+
*/
6+
export function addJsDoc() {
7+
return {
8+
name: "add-js-doc",
9+
/**
10+
* @param {object} params
11+
* @param {typeof import("typescript")} params.ts
12+
* @param {import('typescript').Node} params.node
13+
* @param {import('@custom-elements-manifest/analyzer').AnalyzePhaseParams.moduleDoc} params.moduleDoc
14+
*/
15+
analyzePhase({ ts, node, moduleDoc }) {
16+
if (node.kind === ts.SyntaxKind.ClassDeclaration) {
17+
const className = node.name.getText();
18+
const classDoc = moduleDoc?.declarations?.find(
19+
declaration => declaration.name === className
20+
);
21+
const customTags = ["tag", "summary", "cssproperty"];
22+
let customComments = "/**";
23+
24+
node.jsDoc?.forEach(jsDoc => {
25+
jsDoc?.tags?.forEach(tag => {
26+
const tagName = tag.tagName.getText();
27+
28+
if (customTags.includes(tagName)) {
29+
customComments += `\n * @${tagName} ${tag.comment}`;
30+
}
31+
});
32+
});
33+
34+
// This is what allows us to map JSDOC comments to ReactWrappers.
35+
classDoc["jsDoc"] = node.jsDoc?.map(jsDoc => jsDoc.getFullText()).join("\n");
36+
37+
const parsed = parse(`${customComments}\n */`);
38+
39+
if (!parsed.length) return;
40+
41+
parsed[0].tags?.forEach(t => {
42+
if (!Array.isArray(classDoc[t.tag])) {
43+
classDoc[t.tag] = [];
44+
}
45+
46+
classDoc[t.tag].push({
47+
name: t.name,
48+
description: t.description,
49+
type: t.type || undefined,
50+
});
51+
});
52+
}
53+
},
54+
};
55+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* @typedef {Object} CommonOptions
3+
* @property {import("typescript").TypeChecker} typeChecker
4+
*/
5+
import { Resolver } from "./utils/Resolver.js";
6+
7+
/**
8+
* @param {CommonOptions} opts
9+
* @returns {import('@custom-elements-manifest/analyzer').Plugin}
10+
*/
11+
export function decoratedEventCollector(opts) {
12+
return {
13+
name: "decorated-event-collector",
14+
/**
15+
* @param {object} params
16+
* @param {typeof import("typescript")} params.ts
17+
* @param {import('typescript').Node} params.node
18+
* @param {import('@custom-elements-manifest/analyzer').AnalyzePhaseParams.moduleDoc} params.moduleDoc
19+
*/
20+
analyzePhase({ ts, node, moduleDoc, context }) {
21+
if (ts.isPropertyDeclaration(node) && node.modifiers) {
22+
const classDecl = node.parent;
23+
const className = classDecl.name?.getText?.();
24+
const classDoc = moduleDoc.declarations?.find(
25+
d => d.kind === "class" && d.name === className
26+
);
27+
if (!classDoc) return;
28+
29+
const decorators = node.modifiers.filter(m => m.kind === ts.SyntaxKind.Decorator);
30+
for (const decorator of decorators) {
31+
const expr = decorator.expression;
32+
const isEventDecorator =
33+
ts.isCallExpression(expr) &&
34+
ts.isIdentifier(expr.expression) &&
35+
expr.expression.getText() === "event";
36+
37+
if (!isEventDecorator) continue;
38+
39+
const arg = expr.arguments?.[0];
40+
const eventName = ts.isStringLiteral(arg) || ts.isIdentifier(arg) ? arg.text : null;
41+
if (!eventName) continue;
42+
43+
const resolver = new Resolver({
44+
ts,
45+
typeChecker: opts.typeChecker,
46+
classDeclaration: classDecl,
47+
context,
48+
});
49+
50+
const [typeText, parsedType] = resolveEventDetailType(resolver, node.type);
51+
52+
classDoc.events ??= [];
53+
classDoc.events.push({
54+
name: eventName,
55+
type: {
56+
text: `CustomEvent<${typeText}>`,
57+
},
58+
description: resolveDescription(node.jsDoc),
59+
parsedType: {
60+
text: parsedType,
61+
},
62+
});
63+
64+
classDoc.members = (classDoc.members ?? []).filter(m => m.name !== node.name?.getText());
65+
}
66+
}
67+
},
68+
};
69+
}
70+
71+
/**
72+
* @param {import("typescript").JSDoc[] | undefined} jsDoc
73+
* @return {string | undefined}
74+
*/
75+
function resolveDescription(jsDoc) {
76+
if (jsDoc?.length) {
77+
return jsDoc[0].comment?.toString();
78+
}
79+
return undefined;
80+
}
81+
82+
/**
83+
* @param {Resolver} resolver
84+
* @param {import("typescript").TypeNode} type
85+
* @returns {[string, string]}
86+
*/
87+
function resolveEventDetailType(resolver, type) {
88+
// has generics
89+
if (resolver.ts.isTypeReferenceNode(type) && type.typeArguments?.length) {
90+
const eventType = type.typeArguments[0];
91+
return [eventType.getText(), resolver.resolveTypeNode(eventType)];
92+
}
93+
94+
return [type.getText(), type.getText()];
95+
}

cemPlugins/generateReactExports.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2+
import { join as pathJoin } from "node:path";
3+
import { pascalCase } from "pascal-case";
4+
import { format } from "prettier";
5+
import { resolveFilePath } from "./utils/resolveFilePath.js";
6+
import { resolveParsedType } from "./utils/resolveParsedType.js";
7+
8+
const prettierConfig = JSON.parse(readFileSync(".prettierrc.json", "utf-8"));
9+
10+
/**
11+
* @returns {import('@custom-elements-manifest/analyzer').Plugin}
12+
*/
13+
export function generateReactExports() {
14+
return {
15+
name: "generate-react-exports",
16+
/**
17+
* @param {object} params
18+
* @param {import("custom-elements-manifest").Package} params.customElementsManifest
19+
*/
20+
packageLinkPhase({ customElementsManifest }) {
21+
const components = customElementsManifest.modules.map(mod => {
22+
return [mod.declarations.find(d => d.customElement || !!d.tagName), mod.path];
23+
});
24+
25+
if (!components.length) {
26+
throw new Error("Component not found!");
27+
}
28+
29+
const componentsCode = components
30+
.map(([el, path]) => resolveComponent(el, path))
31+
.filter(c => !!c[0])
32+
.join("\n");
33+
34+
const code = `import React from "react";
35+
import { createComponent } from "@lit-labs/react";
36+
37+
type Constructor<T> = { new (): T };
38+
${componentsCode}`;
39+
40+
const formattedCode = format(code, Object.assign(prettierConfig, { parser: "typescript" }));
41+
const outputPath = "./src";
42+
mkdirSync(outputPath, { recursive: true });
43+
writeFileSync(pathJoin(outputPath, "baklava-react.ts"), formattedCode, { encoding: "utf-8" });
44+
},
45+
};
46+
}
47+
48+
/**
49+
* @param el {import("custom-elements-manifest").MixinDeclaration}
50+
* @param path string
51+
* @return string
52+
*/
53+
function resolveComponent(el, path) {
54+
const resolvedPath = resolveFilePath(path, "default", "./");
55+
const { exportCodes, fieldCodes } = resolveEvents(el.events, el.name);
56+
57+
return `
58+
export type ${el.name} = ${resolvedPath};
59+
${exportCodes}
60+
61+
${el.jsDoc || ""}
62+
export const ${el.name} = React.lazy(() =>
63+
customElements.whenDefined("${el.tagName}").then(() => ({
64+
default: createComponent({
65+
react: React,
66+
displayName: "${el.name}",
67+
tagName: "${el.tagName}",
68+
elementClass: customElements.get("${el.name}") as Constructor<${resolvedPath}>,
69+
${fieldCodes ? `events: {${fieldCodes}}` : ""}
70+
})
71+
}))
72+
);`;
73+
}
74+
75+
/**
76+
* @param events {(import("custom-elements-manifest").Event & {parsedType: import("custom-elements-manifest").Type})[] | null}
77+
* @param componentName {string}
78+
* @return {{exportCodes: string, fieldCodes: string}}
79+
*/
80+
function resolveEvents(events, componentName) {
81+
if (!events) {
82+
return { exportCodes: "", fieldCodes: "" };
83+
}
84+
85+
const exportCodes = [];
86+
const fieldCodes = [];
87+
for (const event of events) {
88+
const pascalCaseEventName = pascalCase(event.name);
89+
const exportedEventName = `${componentName}${pascalCaseEventName.replace(/^Bl/, "")}`;
90+
const reactEventName = `on${pascalCaseEventName}`;
91+
const resolvedEventType = resolveParsedType(event.parsedType.text, "./") ?? "any";
92+
93+
exportCodes.push(`export type ${exportedEventName} = CustomEvent<${resolvedEventType}>;`);
94+
}
95+
96+
return { exportCodes: exportCodes.join("\n"), fieldCodes: fieldCodes.join("\n,") };
97+
}

0 commit comments

Comments
 (0)