chore: add auto-format workflow and fix formatting (#319)

- Add GitHub Action to auto-format PRs with Biome
- Fix formatting in app/manifest.ts and scripts/test-diagram-operations.mjs
This commit is contained in:
Dayuan Jiang
2025-12-18 23:03:08 +09:00
committed by GitHub
parent a91bd9d1e8
commit 6bb33eeda2
3 changed files with 235 additions and 63 deletions

47
.github/workflows/auto-format.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Auto Format
on:
pull_request:
types: [opened, synchronize]
permissions:
contents: write
jobs:
format:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Biome
run: npm install --save-dev @biomejs/biome
- name: Run Biome format
run: npx @biomejs/biome check --write --no-errors-on-unmatched .
- name: Check for changes
id: changes
run: |
if git diff --quiet; then
echo "has_changes=false" >> $GITHUB_OUTPUT
else
echo "has_changes=true" >> $GITHUB_OUTPUT
fi
- name: Commit changes
if: steps.changes.outputs.has_changes == 'true'
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git add .
git commit -m "style: auto-format with Biome"
git push

View File

@@ -1,27 +1,28 @@
import type { MetadataRoute } from "next"; import type { MetadataRoute } from "next"
export default function manifest(): MetadataRoute.Manifest { export default function manifest(): MetadataRoute.Manifest {
return { return {
name: 'Next AI Draw.io', name: "Next AI Draw.io",
short_name: 'AIDraw.io', short_name: "AIDraw.io",
description: 'Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.', description:
start_url: '/', "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
display: 'standalone', start_url: "/",
background_color: '#f9fafb', display: "standalone",
theme_color: '#171d26', background_color: "#f9fafb",
icons: [ theme_color: "#171d26",
{ icons: [
src: '/favicon-192x192.png', {
sizes: '192x192', src: "/favicon-192x192.png",
type: 'image/png', sizes: "192x192",
purpose: 'any', type: "image/png",
}, purpose: "any",
{ },
src: '/favicon-512x512.png', {
sizes: '512x512', src: "/favicon-512x512.png",
type: 'image/png', sizes: "512x512",
purpose: 'any', type: "image/png",
}, purpose: "any",
], },
} ],
}
} }

View File

@@ -20,7 +20,13 @@ function applyDiagramOperations(xmlContent, operations) {
if (parseError) { if (parseError) {
return { return {
result: xmlContent, result: xmlContent,
errors: [{ type: "update", cellId: "", message: `XML parse error: ${parseError.textContent}` }], errors: [
{
type: "update",
cellId: "",
message: `XML parse error: ${parseError.textContent}`,
},
],
} }
} }
@@ -28,7 +34,13 @@ function applyDiagramOperations(xmlContent, operations) {
if (!root) { if (!root) {
return { return {
result: xmlContent, result: xmlContent,
errors: [{ type: "update", cellId: "", message: "Could not find <root> element in XML" }], errors: [
{
type: "update",
cellId: "",
message: "Could not find <root> element in XML",
},
],
} }
} }
@@ -42,22 +54,41 @@ function applyDiagramOperations(xmlContent, operations) {
if (op.type === "update") { if (op.type === "update") {
const existingCell = cellMap.get(op.cell_id) const existingCell = cellMap.get(op.cell_id)
if (!existingCell) { if (!existingCell) {
errors.push({ type: "update", cellId: op.cell_id, message: `Cell with id="${op.cell_id}" not found` }) errors.push({
type: "update",
cellId: op.cell_id,
message: `Cell with id="${op.cell_id}" not found`,
})
continue continue
} }
if (!op.new_xml) { if (!op.new_xml) {
errors.push({ type: "update", cellId: op.cell_id, message: "new_xml is required for update operation" }) errors.push({
type: "update",
cellId: op.cell_id,
message: "new_xml is required for update operation",
})
continue continue
} }
const newDoc = parser.parseFromString(`<wrapper>${op.new_xml}</wrapper>`, "text/xml") const newDoc = parser.parseFromString(
`<wrapper>${op.new_xml}</wrapper>`,
"text/xml",
)
const newCell = newDoc.querySelector("mxCell") const newCell = newDoc.querySelector("mxCell")
if (!newCell) { if (!newCell) {
errors.push({ type: "update", cellId: op.cell_id, message: "new_xml must contain an mxCell element" }) errors.push({
type: "update",
cellId: op.cell_id,
message: "new_xml must contain an mxCell element",
})
continue continue
} }
const newCellId = newCell.getAttribute("id") const newCellId = newCell.getAttribute("id")
if (newCellId !== op.cell_id) { if (newCellId !== op.cell_id) {
errors.push({ type: "update", cellId: op.cell_id, message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"` }) errors.push({
type: "update",
cellId: op.cell_id,
message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"`,
})
continue continue
} }
const importedNode = doc.importNode(newCell, true) const importedNode = doc.importNode(newCell, true)
@@ -65,22 +96,41 @@ function applyDiagramOperations(xmlContent, operations) {
cellMap.set(op.cell_id, importedNode) cellMap.set(op.cell_id, importedNode)
} else if (op.type === "add") { } else if (op.type === "add") {
if (cellMap.has(op.cell_id)) { if (cellMap.has(op.cell_id)) {
errors.push({ type: "add", cellId: op.cell_id, message: `Cell with id="${op.cell_id}" already exists` }) errors.push({
type: "add",
cellId: op.cell_id,
message: `Cell with id="${op.cell_id}" already exists`,
})
continue continue
} }
if (!op.new_xml) { if (!op.new_xml) {
errors.push({ type: "add", cellId: op.cell_id, message: "new_xml is required for add operation" }) errors.push({
type: "add",
cellId: op.cell_id,
message: "new_xml is required for add operation",
})
continue continue
} }
const newDoc = parser.parseFromString(`<wrapper>${op.new_xml}</wrapper>`, "text/xml") const newDoc = parser.parseFromString(
`<wrapper>${op.new_xml}</wrapper>`,
"text/xml",
)
const newCell = newDoc.querySelector("mxCell") const newCell = newDoc.querySelector("mxCell")
if (!newCell) { if (!newCell) {
errors.push({ type: "add", cellId: op.cell_id, message: "new_xml must contain an mxCell element" }) errors.push({
type: "add",
cellId: op.cell_id,
message: "new_xml must contain an mxCell element",
})
continue continue
} }
const newCellId = newCell.getAttribute("id") const newCellId = newCell.getAttribute("id")
if (newCellId !== op.cell_id) { if (newCellId !== op.cell_id) {
errors.push({ type: "add", cellId: op.cell_id, message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"` }) errors.push({
type: "add",
cellId: op.cell_id,
message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"`,
})
continue continue
} }
const importedNode = doc.importNode(newCell, true) const importedNode = doc.importNode(newCell, true)
@@ -89,7 +139,11 @@ function applyDiagramOperations(xmlContent, operations) {
} else if (op.type === "delete") { } else if (op.type === "delete") {
const existingCell = cellMap.get(op.cell_id) const existingCell = cellMap.get(op.cell_id)
if (!existingCell) { if (!existingCell) {
errors.push({ type: "delete", cellId: op.cell_id, message: `Cell with id="${op.cell_id}" not found` }) errors.push({
type: "delete",
cellId: op.cell_id,
message: `Cell with id="${op.cell_id}" not found`,
})
continue continue
} }
existingCell.parentNode?.removeChild(existingCell) existingCell.parentNode?.removeChild(existingCell)
@@ -149,28 +203,52 @@ test("Update operation changes cell value", () => {
{ {
type: "update", type: "update",
cell_id: "2", cell_id: "2",
new_xml: '<mxCell id="2" value="Updated Box A" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="120" height="60" as="geometry"/></mxCell>', new_xml:
'<mxCell id="2" value="Updated Box A" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="120" height="60" as="geometry"/></mxCell>',
}, },
]) ])
assert(errors.length === 0, `Expected no errors, got: ${JSON.stringify(errors)}`) assert(
assert(result.includes('value="Updated Box A"'), "Updated value should be in result") errors.length === 0,
assert(!result.includes('value="Box A"'), "Old value should not be in result") `Expected no errors, got: ${JSON.stringify(errors)}`,
)
assert(
result.includes('value="Updated Box A"'),
"Updated value should be in result",
)
assert(
!result.includes('value="Box A"'),
"Old value should not be in result",
)
}) })
test("Update operation fails for non-existent cell", () => { test("Update operation fails for non-existent cell", () => {
const { errors } = applyDiagramOperations(sampleXml, [ const { errors } = applyDiagramOperations(sampleXml, [
{ type: "update", cell_id: "999", new_xml: '<mxCell id="999" value="Test"/>' }, {
type: "update",
cell_id: "999",
new_xml: '<mxCell id="999" value="Test"/>',
},
]) ])
assert(errors.length === 1, "Should have one error") assert(errors.length === 1, "Should have one error")
assert(errors[0].message.includes("not found"), "Error should mention not found") assert(
errors[0].message.includes("not found"),
"Error should mention not found",
)
}) })
test("Update operation fails on ID mismatch", () => { test("Update operation fails on ID mismatch", () => {
const { errors } = applyDiagramOperations(sampleXml, [ const { errors } = applyDiagramOperations(sampleXml, [
{ type: "update", cell_id: "2", new_xml: '<mxCell id="WRONG" value="Test"/>' }, {
type: "update",
cell_id: "2",
new_xml: '<mxCell id="WRONG" value="Test"/>',
},
]) ])
assert(errors.length === 1, "Should have one error") assert(errors.length === 1, "Should have one error")
assert(errors[0].message.includes("ID mismatch"), "Error should mention ID mismatch") assert(
errors[0].message.includes("ID mismatch"),
"Error should mention ID mismatch",
)
}) })
test("Add operation creates new cell", () => { test("Add operation creates new cell", () => {
@@ -178,41 +256,72 @@ test("Add operation creates new cell", () => {
{ {
type: "add", type: "add",
cell_id: "new1", cell_id: "new1",
new_xml: '<mxCell id="new1" value="New Box" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="500" y="100" width="120" height="60" as="geometry"/></mxCell>', new_xml:
'<mxCell id="new1" value="New Box" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="500" y="100" width="120" height="60" as="geometry"/></mxCell>',
}, },
]) ])
assert(errors.length === 0, `Expected no errors, got: ${JSON.stringify(errors)}`) assert(
errors.length === 0,
`Expected no errors, got: ${JSON.stringify(errors)}`,
)
assert(result.includes('id="new1"'), "New cell should be in result") assert(result.includes('id="new1"'), "New cell should be in result")
assert(result.includes('value="New Box"'), "New cell value should be in result") assert(
result.includes('value="New Box"'),
"New cell value should be in result",
)
}) })
test("Add operation fails for duplicate ID", () => { test("Add operation fails for duplicate ID", () => {
const { errors } = applyDiagramOperations(sampleXml, [ const { errors } = applyDiagramOperations(sampleXml, [
{ type: "add", cell_id: "2", new_xml: '<mxCell id="2" value="Duplicate"/>' }, {
type: "add",
cell_id: "2",
new_xml: '<mxCell id="2" value="Duplicate"/>',
},
]) ])
assert(errors.length === 1, "Should have one error") assert(errors.length === 1, "Should have one error")
assert(errors[0].message.includes("already exists"), "Error should mention already exists") assert(
errors[0].message.includes("already exists"),
"Error should mention already exists",
)
}) })
test("Add operation fails on ID mismatch", () => { test("Add operation fails on ID mismatch", () => {
const { errors } = applyDiagramOperations(sampleXml, [ const { errors } = applyDiagramOperations(sampleXml, [
{ type: "add", cell_id: "new1", new_xml: '<mxCell id="WRONG" value="Test"/>' }, {
type: "add",
cell_id: "new1",
new_xml: '<mxCell id="WRONG" value="Test"/>',
},
]) ])
assert(errors.length === 1, "Should have one error") assert(errors.length === 1, "Should have one error")
assert(errors[0].message.includes("ID mismatch"), "Error should mention ID mismatch") assert(
errors[0].message.includes("ID mismatch"),
"Error should mention ID mismatch",
)
}) })
test("Delete operation removes cell", () => { test("Delete operation removes cell", () => {
const { result, errors } = applyDiagramOperations(sampleXml, [{ type: "delete", cell_id: "3" }]) const { result, errors } = applyDiagramOperations(sampleXml, [
assert(errors.length === 0, `Expected no errors, got: ${JSON.stringify(errors)}`) { type: "delete", cell_id: "3" },
])
assert(
errors.length === 0,
`Expected no errors, got: ${JSON.stringify(errors)}`,
)
assert(!result.includes('id="3"'), "Deleted cell should not be in result") assert(!result.includes('id="3"'), "Deleted cell should not be in result")
assert(result.includes('id="2"'), "Other cells should remain") assert(result.includes('id="2"'), "Other cells should remain")
}) })
test("Delete operation fails for non-existent cell", () => { test("Delete operation fails for non-existent cell", () => {
const { errors } = applyDiagramOperations(sampleXml, [{ type: "delete", cell_id: "999" }]) const { errors } = applyDiagramOperations(sampleXml, [
{ type: "delete", cell_id: "999" },
])
assert(errors.length === 1, "Should have one error") assert(errors.length === 1, "Should have one error")
assert(errors[0].message.includes("not found"), "Error should mention not found") assert(
errors[0].message.includes("not found"),
"Error should mention not found",
)
}) })
test("Multiple operations in sequence", () => { test("Multiple operations in sequence", () => {
@@ -220,30 +329,45 @@ test("Multiple operations in sequence", () => {
{ {
type: "update", type: "update",
cell_id: "2", cell_id: "2",
new_xml: '<mxCell id="2" value="Updated" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="120" height="60" as="geometry"/></mxCell>', new_xml:
'<mxCell id="2" value="Updated" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="100" y="100" width="120" height="60" as="geometry"/></mxCell>',
}, },
{ {
type: "add", type: "add",
cell_id: "new1", cell_id: "new1",
new_xml: '<mxCell id="new1" value="Added" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="500" y="100" width="120" height="60" as="geometry"/></mxCell>', new_xml:
'<mxCell id="new1" value="Added" style="rounded=1;" vertex="1" parent="1"><mxGeometry x="500" y="100" width="120" height="60" as="geometry"/></mxCell>',
}, },
{ type: "delete", cell_id: "3" }, { type: "delete", cell_id: "3" },
]) ])
assert(errors.length === 0, `Expected no errors, got: ${JSON.stringify(errors)}`) assert(
assert(result.includes('value="Updated"'), "Updated value should be present") errors.length === 0,
`Expected no errors, got: ${JSON.stringify(errors)}`,
)
assert(
result.includes('value="Updated"'),
"Updated value should be present",
)
assert(result.includes('id="new1"'), "Added cell should be present") assert(result.includes('id="new1"'), "Added cell should be present")
assert(!result.includes('id="3"'), "Deleted cell should not be present") assert(!result.includes('id="3"'), "Deleted cell should not be present")
}) })
test("Invalid XML returns parse error", () => { test("Invalid XML returns parse error", () => {
const { errors } = applyDiagramOperations("<not valid xml", [{ type: "delete", cell_id: "1" }]) const { errors } = applyDiagramOperations("<not valid xml", [
{ type: "delete", cell_id: "1" },
])
assert(errors.length === 1, "Should have one error") assert(errors.length === 1, "Should have one error")
}) })
test("Missing root element returns error", () => { test("Missing root element returns error", () => {
const { errors } = applyDiagramOperations("<mxfile></mxfile>", [{ type: "delete", cell_id: "1" }]) const { errors } = applyDiagramOperations("<mxfile></mxfile>", [
{ type: "delete", cell_id: "1" },
])
assert(errors.length === 1, "Should have one error") assert(errors.length === 1, "Should have one error")
assert(errors[0].message.includes("root"), "Error should mention root element") assert(
errors[0].message.includes("root"),
"Error should mention root element",
)
}) })
// Summary // Summary