refactor: projet stand-alone sans dépendance aoe_technology_radar
- Intégration du code source du framework dans radar-app/ (vendoring) - Suppression de la dépendance npm aoe_technology_radar - Création de scripts build-radar.js et serve-radar.js pour remplacer le CLI techradar - Adaptation de tous les scripts et Docker pour utiliser radar-app/ au lieu de .techradar - Refactorisation complète de Dockerfile.business - Mise à jour de la documentation (architecture, déploiement, développement) - Mise à jour de .gitignore pour ignorer les artefacts de build de radar-app/ - Ajout de postcss dans les dépendances Docker pour le build Next.js Le projet est maintenant complètement indépendant du package externe. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
271
radar-app/scripts/buildData.ts
Normal file
271
radar-app/scripts/buildData.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import fs from "fs";
|
||||
import matter from "gray-matter";
|
||||
import hljs from "highlight.js";
|
||||
import { Marked } from "marked";
|
||||
import { markedHighlight } from "marked-highlight";
|
||||
import path from "path";
|
||||
|
||||
import nextConfig from "../next.config.js";
|
||||
import config from "../src/lib/config";
|
||||
import ErrorHandler, { ErrorType, TextColor } from "./errorHandler.js";
|
||||
import Positioner from "./positioner";
|
||||
|
||||
import { Flag, Item } from "@/lib/types";
|
||||
|
||||
const {
|
||||
rings,
|
||||
chart: { size },
|
||||
} = config;
|
||||
|
||||
const ringIds = rings.map((r) => r.id);
|
||||
const quadrants = config.quadrants.map((q, i) => ({ ...q, position: i + 1 }));
|
||||
const quadrantIds = quadrants.map((q) => q.id);
|
||||
const tags = (config as { tags?: string[] }).tags || [];
|
||||
const positioner = new Positioner(size, quadrants, rings);
|
||||
const errorHandler = new ErrorHandler(quadrants, rings);
|
||||
|
||||
const marked = new Marked(
|
||||
markedHighlight({
|
||||
langPrefix: "hljs language-",
|
||||
highlight(code, lang, info) {
|
||||
const language = hljs.getLanguage(lang) ? lang : "plaintext";
|
||||
return hljs.highlight(code, { language }).value;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
function dataPath(...paths: string[]): string {
|
||||
return path.resolve("data", ...paths);
|
||||
}
|
||||
|
||||
function convertToHtml(markdown: string): string {
|
||||
// replace deprecated internal links with .html extension
|
||||
markdown = markdown.replace(/(]\(\/[^)]+)\.html/g, "$1/");
|
||||
|
||||
if (nextConfig.basePath) {
|
||||
markdown = markdown.replace(/]\(\//g, `](${nextConfig.basePath}/`);
|
||||
}
|
||||
|
||||
let html = marked.parse(markdown.trim()) as string;
|
||||
html = html.replace(
|
||||
/a href="http/g,
|
||||
'a target="_blank" rel="noopener noreferrer" href="http',
|
||||
);
|
||||
return html;
|
||||
}
|
||||
|
||||
function readMarkdownFile(filePath: string) {
|
||||
const id = path.basename(filePath, ".md");
|
||||
const fileContent = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
try {
|
||||
const { data, content } = matter(fileContent);
|
||||
const body = convertToHtml(content);
|
||||
return { id, data, body };
|
||||
} catch (error) {
|
||||
console.error(`Failed parsing ${filePath}: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to recursively read Markdown files and parse them
|
||||
async function parseDirectory(dirPath: string): Promise<Item[]> {
|
||||
const items: Record<string, Item> = {};
|
||||
|
||||
async function readDir(dirPath: string) {
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await readDir(fullPath);
|
||||
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
||||
const releaseDate = path.basename(path.dirname(fullPath));
|
||||
const { id, data, body } = readMarkdownFile(fullPath);
|
||||
|
||||
if (!items[id]) {
|
||||
items[id] = {
|
||||
id,
|
||||
release: releaseDate,
|
||||
title: data.title || id,
|
||||
ring: data.ring,
|
||||
quadrant: data.quadrant,
|
||||
body,
|
||||
featured: data.featured !== false,
|
||||
flag: Flag.Default,
|
||||
tags: data.tags || [],
|
||||
revisions: [],
|
||||
position: [0, 0],
|
||||
};
|
||||
} else {
|
||||
items[id].release = releaseDate;
|
||||
items[id].body = body || items[id].body;
|
||||
items[id].title = data.title || items[id].title;
|
||||
items[id].ring = data.ring || items[id].ring;
|
||||
items[id].quadrant = data.quadrant || items[id].quadrant;
|
||||
items[id].tags = data.tags || items[id].tags;
|
||||
items[id].featured =
|
||||
typeof data.featured === "boolean"
|
||||
? data.featured
|
||||
: items[id].featured;
|
||||
}
|
||||
|
||||
items[id].revisions!.push({
|
||||
release: releaseDate,
|
||||
ring: data.ring,
|
||||
body,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await readDir(dirPath);
|
||||
return Object.values(items).sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
|
||||
function getUniqueReleases(items: Item[]): string[] {
|
||||
const releases = new Set<string>();
|
||||
for (const item of items) {
|
||||
for (const revision of item.revisions || []) {
|
||||
releases.add(revision.release);
|
||||
}
|
||||
}
|
||||
return Array.from(releases).sort();
|
||||
}
|
||||
|
||||
function getUniqueTags(items: Item[]): string[] {
|
||||
const tags = new Set<string>();
|
||||
for (const item of items) {
|
||||
for (const tag of item.tags || []) {
|
||||
tags.add(tag);
|
||||
}
|
||||
}
|
||||
return Array.from(tags).sort();
|
||||
}
|
||||
|
||||
function getFlag(item: Item, allReleases: string[]): Flag {
|
||||
// return default flag if this is the first edition of the radar
|
||||
if (allReleases.length === 1) {
|
||||
return Flag.Default;
|
||||
}
|
||||
|
||||
const latestRelease = allReleases[allReleases.length - 1];
|
||||
const revisions = item.revisions || [];
|
||||
const isInLatestRelease =
|
||||
revisions.length > 0 &&
|
||||
revisions[revisions.length - 1].release === latestRelease;
|
||||
|
||||
if (revisions.length == 1 && isInLatestRelease) {
|
||||
return Flag.New;
|
||||
} else if (revisions.length > 1 && isInLatestRelease) {
|
||||
return Flag.Changed;
|
||||
}
|
||||
|
||||
return Flag.Default;
|
||||
}
|
||||
|
||||
function postProcessItems(items: Item[]): {
|
||||
releases: string[];
|
||||
tags: string[];
|
||||
items: Item[];
|
||||
} {
|
||||
const filteredItems = items.filter((item) => {
|
||||
// check if the items' quadrant and ring are valid
|
||||
if (!item.quadrant || !item.ring) {
|
||||
errorHandler.processBuildErrors(ErrorType.NoQuadrant, item.id);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!quadrantIds.includes(item.quadrant)) {
|
||||
errorHandler.processBuildErrors(
|
||||
ErrorType.InvalidQuadrant,
|
||||
item.id,
|
||||
item.quadrant,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ringIds.includes(item.ring)) {
|
||||
errorHandler.processBuildErrors(
|
||||
ErrorType.InvalidRing,
|
||||
item.id,
|
||||
item.ring,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if config has a key `tags` and if it is an array
|
||||
if (Array.isArray(tags) && tags.length) {
|
||||
// if tags are specified, only keep items that have at least one of the tags
|
||||
return item.tags?.some((tag) => tags.includes(tag));
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
errorHandler.checkForBuildErrors();
|
||||
|
||||
const releases = getUniqueReleases(filteredItems);
|
||||
const uniqueTags = getUniqueTags(filteredItems);
|
||||
const processedItems = filteredItems.map((item) => {
|
||||
const processedItem = {
|
||||
...item,
|
||||
position: positioner.getNextPosition(item.quadrant, item.ring),
|
||||
flag: getFlag(item, releases),
|
||||
// only keep revision which ring or body is different
|
||||
revisions: item.revisions
|
||||
?.filter((revision, index, revisions) => {
|
||||
const { ring, body } = revision;
|
||||
return (
|
||||
ring !== item.ring ||
|
||||
(body != "" &&
|
||||
body != item.body &&
|
||||
body !== revisions[index - 1]?.body)
|
||||
);
|
||||
})
|
||||
.reverse(),
|
||||
};
|
||||
|
||||
// unset revisions if there are none
|
||||
if (!processedItem.revisions?.length) {
|
||||
delete processedItem.revisions;
|
||||
}
|
||||
|
||||
// unset tags if there are none
|
||||
if (!processedItem.tags?.length) {
|
||||
delete processedItem.tags;
|
||||
}
|
||||
|
||||
return processedItem;
|
||||
});
|
||||
|
||||
return { releases, tags: uniqueTags, items: processedItems };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Parse the data and write radar data to JSON file
|
||||
const items = await parseDirectory(dataPath("radar"));
|
||||
const data = postProcessItems(items);
|
||||
|
||||
if (data.items.length === 0) {
|
||||
errorHandler.processBuildErrors(ErrorType.NoRadarItems);
|
||||
}
|
||||
|
||||
errorHandler.checkForBuildErrors(true);
|
||||
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
fs.writeFileSync(dataPath("data.json"), json);
|
||||
|
||||
// write about data to JSON file
|
||||
const about = readMarkdownFile(dataPath("about.md"));
|
||||
fs.writeFileSync(dataPath("about.json"), JSON.stringify(about, null, 2));
|
||||
console.log(
|
||||
"ℹ️ Data written to data/data.json and data/about.json\n\n" +
|
||||
errorHandler.colorizeBackground(
|
||||
" Build was successfull ",
|
||||
TextColor.Green,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
108
radar-app/scripts/errorHandler.ts
Normal file
108
radar-app/scripts/errorHandler.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Quadrant, Ring } from "@/lib/types";
|
||||
|
||||
export enum ErrorType {
|
||||
NoQuadrant = "Item {0} has no quadrant or ring",
|
||||
InvalidQuadrant = "Item {0} has invalid quadrant {1}\n\tvalid quadrants are: {2}",
|
||||
InvalidRing = "Item {0} has invalid ring {1}\n\tvalid rings are: {2}",
|
||||
NoRadarItems = "No valid radar items found. Please check the markdown files in the `radar` directory.",
|
||||
}
|
||||
|
||||
export enum TextColor {
|
||||
Default = 0,
|
||||
Black,
|
||||
Red = 31,
|
||||
Green = 32,
|
||||
Yellow = 33,
|
||||
Blue = 34,
|
||||
Mangenta = 35,
|
||||
Cyan = 36,
|
||||
White = 37,
|
||||
}
|
||||
|
||||
export default class ErrorHandler {
|
||||
private buildErrors: string[] = [];
|
||||
private quadrants: Quadrant[];
|
||||
private rings: Ring[];
|
||||
private isStrict: boolean;
|
||||
private supportsColor: boolean;
|
||||
|
||||
constructor(quadrants: Quadrant[], rings: Ring[]) {
|
||||
this.isStrict = process.argv.slice(2).includes("--strict");
|
||||
this.supportsColor = process.stdout.isTTY && process.env.TERM !== "dumb";
|
||||
this.quadrants = quadrants;
|
||||
this.rings = rings;
|
||||
console.log(`ℹ️ Build is${this.isStrict ? "" : " not"} in strict mode\n`);
|
||||
}
|
||||
|
||||
public processBuildErrors(errorType: ErrorType, ...args: string[]) {
|
||||
const errorHint = this.getErrorHint(errorType);
|
||||
const errorMsg = this.formatString(
|
||||
errorType.toString(),
|
||||
errorHint ? [...args, errorHint] : args,
|
||||
);
|
||||
this.buildErrors.push(errorMsg);
|
||||
}
|
||||
|
||||
public checkForBuildErrors(exitOnErr: boolean = false) {
|
||||
if (this.buildErrors.length > 0) {
|
||||
console.warn(
|
||||
this.colorizeBackground(
|
||||
`There ${this.buildErrors.length > 1 ? "are" : "is"} ${this.buildErrors.length} error${this.buildErrors.length > 1 ? "s" : ""} in your data build`,
|
||||
TextColor.Red,
|
||||
) +
|
||||
"\n\n" +
|
||||
this.buildErrors
|
||||
.map((error, index) => `${index + 1}. ${error}`)
|
||||
.join("\n") +
|
||||
"\n",
|
||||
);
|
||||
|
||||
if (this.isStrict || exitOnErr) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
this.buildErrors = [];
|
||||
}
|
||||
}
|
||||
|
||||
private getErrorHint(errorType: ErrorType) {
|
||||
switch (errorType) {
|
||||
case ErrorType.InvalidQuadrant:
|
||||
return this.quadrants.map((quadrant) => quadrant.id).join(", ");
|
||||
case ErrorType.InvalidRing:
|
||||
return this.rings.map((ring) => ring.id).join(", ");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public colorizeBackground(str: string, backgroundColor: TextColor) {
|
||||
if (this.supportsColor) {
|
||||
return `\x1b[${backgroundColor + 10}m${str}\x1b[${TextColor.Default}m`;
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
private formatString(msg: string, inserts: string[]) {
|
||||
return inserts.reduce(
|
||||
(acc, cur, index) =>
|
||||
acc.replaceAll(
|
||||
`{${index}}`,
|
||||
this.colorizeString(
|
||||
cur,
|
||||
index === 2 ? TextColor.Green : TextColor.Red,
|
||||
),
|
||||
),
|
||||
msg,
|
||||
);
|
||||
}
|
||||
|
||||
private colorizeString(str: string, textColor: TextColor) {
|
||||
if (this.supportsColor) {
|
||||
return `\x1b[${textColor}m${str}\x1b[${TextColor.Default}m`;
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
||||
95
radar-app/scripts/positioner.ts
Normal file
95
radar-app/scripts/positioner.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Quadrant, Ring } from "@/lib/types";
|
||||
|
||||
type Position = [x: number, y: number];
|
||||
type RingDimension = [innerRadius: number, outerRadius: number];
|
||||
|
||||
// Corresponding to positions 1, 2, 3, and 4 respectively
|
||||
const startAngles = [270, 0, 180, 90];
|
||||
|
||||
export default class Positioner {
|
||||
private readonly centerRadius: number;
|
||||
private readonly minDistance: number = 20;
|
||||
private readonly paddingRing: number = 15;
|
||||
private readonly paddingAngle: number = 10;
|
||||
private positions: Record<string, Position[]> = {};
|
||||
private ringDimensions: Record<string, RingDimension> = {};
|
||||
private quadrantAngles: Record<string, number> = {};
|
||||
|
||||
constructor(size: number, quadrants: Quadrant[], rings: Ring[]) {
|
||||
this.centerRadius = size / 2;
|
||||
|
||||
quadrants.forEach((quadrant) => {
|
||||
this.quadrantAngles[quadrant.id] = startAngles[quadrant.position - 1];
|
||||
});
|
||||
|
||||
rings.forEach((ring, index) => {
|
||||
const innerRadius =
|
||||
(rings[index - 1]?.radius ?? 0) * this.centerRadius + this.paddingRing;
|
||||
const outerRadius =
|
||||
(ring.radius ?? 1) * this.centerRadius - this.paddingRing;
|
||||
this.ringDimensions[ring.id] = [innerRadius, outerRadius];
|
||||
});
|
||||
}
|
||||
|
||||
static getDistance(a: Position, b: Position): number {
|
||||
const [x1, y1] = a;
|
||||
const [x2, y2] = b;
|
||||
return Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
|
||||
}
|
||||
|
||||
private isOverlapping(position: Position, positions: Position[]): boolean {
|
||||
return positions.some(
|
||||
(p) => Positioner.getDistance(position, p) < this.minDistance,
|
||||
);
|
||||
}
|
||||
|
||||
private getXYPosition(
|
||||
quadrantId: string,
|
||||
ringId: string,
|
||||
radiusFraction: number,
|
||||
angleFraction: number,
|
||||
): Position {
|
||||
const [innerRadius, outerRadius] = this.ringDimensions[ringId];
|
||||
const ringWidth = outerRadius - innerRadius;
|
||||
const absoluteRadius = innerRadius + radiusFraction * ringWidth;
|
||||
|
||||
const startAngle = this.quadrantAngles[quadrantId] + this.paddingAngle;
|
||||
const endAngle = startAngle + 90 - 2 * this.paddingAngle;
|
||||
const absoluteAngle = startAngle + (endAngle - startAngle) * angleFraction;
|
||||
const angleInRadians = ((absoluteAngle - 90) * Math.PI) / 180;
|
||||
|
||||
return [
|
||||
Math.round(this.centerRadius + absoluteRadius * Math.cos(angleInRadians)),
|
||||
Math.round(this.centerRadius + absoluteRadius * Math.sin(angleInRadians)),
|
||||
];
|
||||
}
|
||||
|
||||
public getNextPosition(quadrantId: string, ringId: string): Position {
|
||||
this.positions[quadrantId] ??= [];
|
||||
|
||||
let tries = 0;
|
||||
let position: Position;
|
||||
|
||||
do {
|
||||
position = this.getXYPosition(
|
||||
quadrantId,
|
||||
ringId,
|
||||
Math.sqrt(Math.random()),
|
||||
Math.random(),
|
||||
);
|
||||
tries++;
|
||||
} while (
|
||||
this.isOverlapping(position, this.positions[quadrantId]) &&
|
||||
tries < 150
|
||||
);
|
||||
|
||||
if (tries >= 150) {
|
||||
console.warn(
|
||||
`Could not find a non-overlapping position for ${quadrantId} in ring ${ringId}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.positions[quadrantId].push(position);
|
||||
return position;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user