Compare commits

..

7 commits

Author SHA1 Message Date
Loren Norman
ade956cdbc fix tooltip punctuation
Some checks failed
Build Docs and Deploy to Pages / build (push) Has been cancelled
Build Docs and Deploy to Pages / Deploy (push) Has been cancelled
2025-08-25 16:35:48 -04:00
Loren Norman
e09f49a345 cleanup 2025-08-22 16:36:19 -04:00
Loren Norman
76393f78c3 block image generation instructions 2025-08-22 16:32:51 -04:00
Loren Norman
3a36c1bb2d better separation for block image task 2025-08-22 15:42:07 -04:00
Loren Norman
d65750fc92 build/preview server info in readme 2025-08-22 14:22:49 -04:00
Loren Norman
9c8d87401a incremental builds, fast dev preview server 2025-08-22 14:17:57 -04:00
Loren Norman
252560ac04
Merge pull request #33 from adafruit/tylerdcooper-patch-1
docs: join text block
2025-08-21 16:17:18 -04:00
11 changed files with 183 additions and 34 deletions

View file

@ -41,6 +41,9 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Export Block Images
run: npm run export:block-images
- name: Export Block Definitions to Markdown
run: npm run docs:export

View file

@ -20,9 +20,18 @@ Node v22.x is expected, but other versions may work.
git clone https://github.com/adafruit/io-actions
cd io-actions
npm i
npm run export:block-images # run once, and whenever images need updating
npm start
```
Now 2 processes should be running:
- a builder process that:
- does a full build of the docs from the app files
- watches app files for changes and updates matching docs
- the docs dev server where you can see your changes rendered live as you save them
When you're done working simply press `CTRL + C` to terminate the processes.
### Exporting
Export a Blockly application:

35
dev_server.js Normal file
View file

@ -0,0 +1,35 @@
import process from 'node:process'
import { exec, execSync, spawnSync, spawn } from 'node:child_process'
import { promisify } from 'node:util'
const execAsync = promisify(exec)
// run a clean documentation build, wait for it to complete
console.log("Building docs from scratch...")
spawnSync("node", ["export.js", "docs"], { stdio: 'inherit' })
// start the file watcher and incremental builder
console.log("Starting incremental builder and file watcher...")
const docBuilder = spawn("node", ["--watch-path=./app", "export.js", "docs-incremental"], { stdio: 'inherit' })
docBuilder.on('error', err => console.log('Builder Error:', err))
docBuilder.on('exit', code => console.log('Builder Exited', code === 0 ? "Cleanly" : `With Error Code ${code}`))
// start the Vitepress docs dev server
console.log("Starting Vitepress docs server...")
const docServer = spawn("npm", ["run", "docs:dev"], { stdio: 'inherit' })
docServer.on('error', err => console.log('Server Error:', err))
docServer.on('exit', code => console.log('Server Exited', code === 0 ? "Cleanly" : `With Error Code ${code}`))
const killAll = () => {
console.log('Shutting down...')
docBuilder.kill()
docServer.kill()
process.exit()
}
// if either one exits, kill the other
console.log("Watching files for changes and servers for crashes")
docServer.on('exit', killAll)
docBuilder.on('exit', killAll)

View file

@ -6,6 +6,10 @@ const REPO = 'https://github.com/adafruit/io-actions'
// https://vitepress.dev/reference/site-config
export default defineConfig({
vite: {
clearScreen: false
},
title: "IO Actions: Block Reference",
description: "Documentation for Adafruit IO's block-based Actions",

44
docs/test.md Normal file
View file

@ -0,0 +1,44 @@
# Markdown Tester
## Works
<span v-pre>
{{ span v-pre }}
::: warning
what a warning!
:::
</span>
```
{{ triple_backticks }}
```
```js
{{ js_triple_backticks }}
```
::: details Embedded in a panel
::: details multiple panels
::: details multiple panels
::: details multiple panels
a message!
:::
## Doesn't Work
<span v-pre>
`{{ single_backticks }}`
</span>
## Experiment
::: v-pre
| Separator |
|-----------|
| <span v-pre>{{ in_span_table }}</span> |
| {{ in_span_table }} |
:::

View file

@ -1,5 +1,5 @@
import { spawn, spawnSync } from 'node:child_process'
import { copyFileSync, cpSync } from 'node:fs'
import { copyFileSync, cpSync, existsSync } from 'node:fs'
import { cleanDir, write, totalBytesWritten } from "./export_util.js"
import DefinitionSet from '#src/definitions/definition_set.js'
@ -20,6 +20,7 @@ const
definitions = await DefinitionSet.load(),
exporters = {
// Build the Blockly application itself
"app": async (destination="export") => {
// clear the export directory
@ -34,13 +35,13 @@ const
})
},
// Build the documentation for the Blockly application
"docs": async () => {
// allow option to skip image generation
const skipImages = taskArgs.includes("skipImages")
if(!skipImages) {
await exporters.blockImages()
cleanDir("docs/block_images")
cpSync("tmp/block_images/images", "docs/block_images", { recursive: true })
// TODO: check and warn if block images haven't been generated
if(!existsSync("docs/block_images/action_root.png")) {
console.log("Block images missing from docs/block_images!")
console.log("Run: `npm run export:block-images` before exporting the docs")
process.exit(1)
}
await exporters.app("docs/blockly")
@ -54,31 +55,50 @@ const
})
},
"blockImages": async () => {
const destination = "tmp/block_images"
cleanDir(destination)
cleanDir(`${destination}/images`)
// Build the documentation but only touch files that have changed
// This is good to pair with a process that watches files for changes
"docs-incremental": async () => {
await exportTo("docs", definitions, exportItem => {
exportItem.blockIndex("blocks/index.md")
exportItem.blockPages()
exportItem.sidebar("blocks/_blocks_sidebar.json")
})
},
// Create png images of all blocks by:
// - creating a temporary app with no toolbox and all blocks on the workspace
// - serving that application
// - driving a browser to it with Cypress
// - right clicking on each block and clicking "download image"
"blockImages": async (imageDestination="docs/block_images") => {
const tmpAppDestination = "tmp/block_images_app"
cleanDir(tmpAppDestination)
// export a special app with no toolbox, all blocks on workspace
await exportTo(destination, definitions, exportItem => {
await exportTo(tmpAppDestination, definitions, exportItem => {
exportItem.workspaceAllBlocks("workspace.json")
write(`${destination}/toolbox.json`, "null")
write(`${tmpAppDestination}/toolbox.json`, "null")
exportItem.blocks("blocks.json")
exportItem.script("blockly_app.js")
// TODO: make a DocumentExporter for generating html wrappers
copyFileSync("src/exporters/document_templates/blockly_workspace.template.html", `${destination}/index.html`)
copyFileSync("src/exporters/document_templates/blockly_workspace.template.html", `${tmpAppDestination}/index.html`)
})
// serve it
// serve the screenshot app
console.log('Serving workspace for screenshots...')
const viteProcess = spawn("npx", ["vite", "serve", destination])
const viteProcess = spawn("npx", ["vite", "serve", tmpAppDestination])
// prepare the image location
cleanDir(imageDestination)
// extract the screenshots
console.log('Generating screenshots...')
spawnSync("npx", ["cypress", "run",
"--config", `downloadsFolder=${destination}/images`,
await spawnSync("npx", ["cypress", "run",
"--config", `downloadsFolder=${imageDestination}`,
"--config-file", `cypress/cypress.config.js`,
])
"--browser", "chromium",
"--spec", "cypress/e2e/block_images.cy.js",
], { stdio: 'inherit' })
console.log('Generation complete.')
// kill the server
@ -86,16 +106,19 @@ const
console.log("Vite failed to exit gracefully")
process.exit(1)
}
console.log('Server closed.')
}
},
exporterNames = Object.keys(exporters)
// Look up the requested exporter
if(!exporterNames.includes(toExport)) {
console.error(`Export Error: No exporter found for: "${toExport}"\nValid exporters: "${exporterNames.join('", "')}"`)
process.exit(1)
}
// Execute the export
const startTime = Date.now()
console.log(`\nStarting Export: ${toExport}`)
console.log("=======================")
@ -105,7 +128,8 @@ await exporter()
const elapsed = Date.now() - startTime
console.log("=======================")
console.log(`🏁 Done. Wrote ${totalBytesWritten.toFixed(3)}k in ${elapsed}ms 🏁`)
console.log(`🏁 Done (${elapsed}ms) 🏁`)
// Bye!
process.exit(0)

View file

@ -11,17 +11,18 @@
},
"main": "index.js",
"scripts": {
"start": "vite --force",
"start": "node dev_server.js",
"test": "node --test",
"test-watch": "node --test --watch",
"test-snapshots": "node --test test/app/blocks/snapshots/block_snapshots_test.js",
"test-update-snapshots": "node --test --test-update-snapshots test/app/blocks/snapshots/block_snapshots_test.js",
"lint": "eslint src/",
"lint-export": "eslint export/",
"export:app": "node export.js app",
"build": "npm run export:app && vite build",
"build-all-branches": "node build_all_branches.js",
"preview": "npm run build && vite preview",
"export:block-images": "node export.js blockImages",
"docs:export": "node export.js docs",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",

View file

@ -194,7 +194,8 @@ BlockDefinition.parseRawDefinition = function(rawBlockDefinition, definitionPath
// take the first line of the description
// blockDef.tooltip = blockDef.description.split("\n")[0]
// take the first sentence of the description
blockDef.tooltip = blockDef.description.split(/\.(\s|$)/)[0] + "."
blockDef.tooltip = blockDef.description.split(/\.(\s|$)/)[0]
if(!blockDef.tooltip.endsWith("?")) { blockDef.tooltip += "." }
blockDef.disabled = !!rawBlockDefinition.disabled
blockDef.connections = rawBlockDefinition.connections
blockDef.template = rawBlockDefinition.template

View file

@ -1,7 +1,6 @@
import { mkdirSync, writeFileSync } from 'fs'
import { dirname } from 'path'
import { forEach, identity, pickBy } from 'lodash-es'
import { writeFileIfDifferent } from '#src/util.js'
import toBlockMarkdown from "#src/docs/render_block.js"
@ -25,10 +24,10 @@ export default class BlockPageExporter {
forEach(this.definitionSet.blocks, blockDefinition => {
const
docPath = options.filenameFunc(blockDefinition),
fullPath = `${this.destination}/${docPath}`
fullPath = `${this.destination}/${docPath}`,
newContent = toBlockMarkdown(blockDefinition)
mkdirSync(dirname(fullPath), { recursive: true })
writeFileSync(fullPath, toBlockMarkdown(blockDefinition))
writeFileIfDifferent(fullPath, newContent)
})
}
@ -41,3 +40,4 @@ export default class BlockPageExporter {
this.export(exportOptions)
}
}

View file

@ -1,5 +1,6 @@
import { writeFileSync } from 'fs'
import { find, forEach, isString, map } from 'lodash-es'
import { find, forEach, isString, map, sortBy } from 'lodash-es'
import { writeFileIfDifferent } from '#src/util.js'
export default class SidebarExporter {
@ -34,7 +35,7 @@ export default class SidebarExporter {
blockSidebar.items.push(uncategorizedCategory)
forEach(this.definitionSet.blocks, blockDefinition => {
forEach(sortBy(this.definitionSet.blocks, 'type'), blockDefinition => {
const
sidebarEntry = {
text: blockDefinition.name,
@ -69,7 +70,7 @@ export default class SidebarExporter {
? options.toFile
: `_blocks_sidebar.json`
writeFileSync(`${this.destination}/${filename}`, JSON.stringify(blockSidebar, null, 2))
writeFileIfDifferent(`${this.destination}/${filename}`, JSON.stringify(blockSidebar, null, 2))
}
exportToFile = (toFile=true) => {

View file

@ -1,6 +1,21 @@
import { dirname } from 'path'
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
import { createHash } from 'crypto'
import { map } from 'lodash-es'
const getStringHash = stringToHash => {
const hash = createHash('sha256')
hash.update(stringToHash)
return hash.digest('hex')
}
const getFileHash = filePath => {
if(!existsSync(filePath)) { return '' }
return getStringHash(readFileSync(filePath))
}
export const
niceTemplate = tplString => {
const
@ -19,4 +34,16 @@ export const
// TODO: support niceties for markdown, double-newlines, escaping, etc
return tplString
},
writeFileIfDifferent = (filename, content) => {
const
fileHash = getFileHash(filename),
contentHash = getStringHash(content)
if(fileHash !== contentHash) {
console.log("writing", filename)
mkdirSync(dirname(filename), { recursive: true })
writeFileSync(filename, content)
}
}