disable vite plugin, delete more dead files

This commit is contained in:
Loren Norman 2025-07-15 14:12:20 -04:00
parent a5a8a078d5
commit 3c9df96d01
13 changed files with 3 additions and 718 deletions

View file

@ -1,110 +0,0 @@
import { glob } from 'glob'
import { compact, keyBy, keys, map, omitBy, sortBy, without } from 'lodash-es'
import { toBlockJSON } from './block_processor/index.js'
// server-side
// gathers all block definition files in /app/blocks
// passes each definition and its path through block processor
// returns a collection of the resulting json
// files
const
PROJECT_ROOT = process.cwd(),
BLOCK_LOCATION = `app/blocks/`,
NON_BLOCK_FILES = [
'**/*extension*.js',
'**/*mixin*.js',
'**/*mutator*.js',
'**/*shadow*.js',
'**/*example*.js',
],
gatherBlockFiles = async () => {
const
jsfiles = await glob(`./${BLOCK_LOCATION}**/*.js`, { ignore: NON_BLOCK_FILES }),
random = Math.random()*100000000 // break the import cache
return Promise.all(
jsfiles.map(
async filePath => ({
definition: (await import(`${PROJECT_ROOT}/${filePath}?key=${random}`)).default,
path: filePath.slice(BLOCK_LOCATION.length)
})
)
)
}
// definitions
const
BLOCK_KEYS = [
"type",
"name",
"primaryCategory",
"color",
"bytecodeKey",
"inputs",
"fields",
"template",
"description",
"docBlocks",
"disabled",
"generators",
"regenerators",
"toolbox",
"visualization",
"mixins",
"extensions",
"mutator",
"connections",
"lines"
],
processBlock = ({ definition, path }) => {
if(!definition) {
throw new Error(`No Block definition found at path: ${BLOCK_LOCATION}${path}`)
}
// ensure only supported keys are present
const extraKeys = without(keys(definition), ...BLOCK_KEYS)
if(extraKeys.length) {
throw new Error(`Custom Block definition has unrecognized keys: "${extraKeys.join('", "')}"\nBlock: ${definition.type} @ ${path}`)
}
if(definition.disabled) { return }
// TODO: mechanism for Definition JSON defaults
if(definition.connections?.mode === 'value') {
// default input values with no output to 'expression'
definition.connections.output = definition.connections.output || 'expression'
}
// TODO: mechanism for Blockly JSON defaults
const blockDefaults = {
inputsInline: false
}
return {
...blockDefaults,
...toBlockJSON(definition)
}
}
export const
importBlockDefinitions = async () =>
omitBy(keyBy(map(await gatherBlockFiles(), "definition"), "type"), def => def.disabled),
importBlockJson = async () =>
sortBy(compact(map(await gatherBlockFiles(), processBlock)), "type"),
// defs and paths
allBlockDefinitionsAndPaths = await gatherBlockFiles(),
// just the definitions
allBlockDefinitions = keyBy(compact(map(allBlockDefinitionsAndPaths, "definition")), "type"),
// without disabled definitions
blockDefinitions = omitBy(allBlockDefinitions, def => def.disabled),
blockJson = compact(map(allBlockDefinitionsAndPaths, processBlock))
export default blockJson

View file

@ -1,29 +0,0 @@
import fs from 'fs'
import { importToolboxJs } from './toolbox_importer.js'
import importMixinsJs from './mixin_importer.js'
import importExtensionsJs from './extension_importer.js'
import importMutatorsJs from './mutator_importer.js'
import importGeneratorsJs from './generator_importer.js'
import importRegeneratorsJs from './regenerator_importer.js'
const section = (title, contents) => `
///////////
// ${title}
///////////
${contents}
`
export default async () => (
[
"import Blockly from 'blockly'\n\n",
section("Toolbox", importToolboxJs()),
section("Mixins", await importMixinsJs()),
section("Extensions", await importExtensionsJs()),
section("Mutators", await importMutatorsJs()),
section("Generators", await importGeneratorsJs()),
section("Regenerators", await importRegeneratorsJs()),
section("Blockly API Wrapper", fs.readFileSync(`./src/importer/blockly_api.js`))
].join("")
)

View file

@ -1,41 +0,0 @@
import { glob } from 'glob'
import { assign, camelCase, fromPairs, isArray } from 'lodash-es'
import { importBlockDefinitions } from './block_importer.js'
import renderTemplate from './template_renderer.js'
import renderObject from './object_renderer.js'
const
PROJECT_ROOT = process.cwd(),
EXTENSION_LOCATION = `app/extensions/`,
gatherExtensionFiles = async () => {
const
jsfiles = await glob(`./${EXTENSION_LOCATION}**/*.js`, { ignore: [ '**/*example*.js' ] }),
random = Math.random()*100000000 // break the import cache
// loads app/extensions/extension_name.js into object like:
// { extensionName: Function }
return fromPairs(await Promise.all(
jsfiles.map( async filePath => ([
camelCase(filePath.split('/').at(-1).slice(0, -3)),
(await import(`${PROJECT_ROOT}/${filePath}?key=${random}`)).default
]))
))
}
export default async () => {
const
fileExtensions = await gatherExtensionFiles(),
allExtensions = Object.values(await importBlockDefinitions())
.map(def => def.extensions)
.filter(ext => ext && !isArray(ext))
.reduce(assign, fileExtensions)
const renderedExtensions = `const allExtensions = ${ renderObject(allExtensions) }`
// render the extensions template and return the output
return renderTemplate(renderedExtensions, './src/importer/extensions.template.js')
}

View file

@ -1,15 +0,0 @@
import { mapValues } from 'lodash-es'
import { importBlockDefinitions } from './block_importer.js'
import renderTemplate from './template_renderer.js'
import renderObject from './object_renderer.js'
export default async () => {
const
blockGenerators = mapValues(await importBlockDefinitions(), "generators"),
renderedGenerators = `const blockGenerators = ${ renderObject(blockGenerators) }`
// render the generators template and return the output
return renderTemplate(renderedGenerators, './src/importer/generators.template.js')
}

View file

@ -1,55 +0,0 @@
import { glob } from 'glob'
import { assign, camelCase, fromPairs, isArray, isString } from 'lodash-es'
import { importBlockDefinitions } from './block_importer.js'
import renderTemplate from './template_renderer.js'
import renderObject from './object_renderer.js'
const
PROJECT_ROOT = process.cwd(),
MIXIN_LOCATION = `app/mixins/`,
gatherMixinFiles = async () => {
const
jsfiles = await glob(`./${MIXIN_LOCATION}**/*.js`, { ignore: [ '**/*example*.js' ] }),
random = Math.random()*100000000 // break the import cache
// loads app/mixins/mixin_name.js into object like:
// { mixinName: Function }
return fromPairs(await Promise.all(
jsfiles.map( async filePath => ([
camelCase(filePath.split('/').at(-1).slice(0, -3)),
(await import(`${PROJECT_ROOT}/${filePath}?key=${random}`)).default
]))
))
}
export default async () => {
const
allMixins = await gatherMixinFiles(),
blockDefMixins = Object.values(await importBlockDefinitions())
.map(def => def.mixins)
.filter(mix => mix)
// merge top-level objects and objects nested under arrays
blockDefMixins.forEach(mixinDef => {
if(isArray(mixinDef)) {
mixinDef.forEach( item => {
// ignore strings
if(!isString(item)) {
// assign all the keys of a nested object
assign(allMixins, item)
}
})
} else {
// assign all the keys of a top-level object
assign(allMixins, mixinDef)
}
})
const renderedMixins = `const allMixins = ${renderObject(allMixins)}`
// render the mixins template and return the output
return renderTemplate(renderedMixins, './src/importer/mixins.template.js')
}

View file

@ -1,15 +0,0 @@
import { mapValues, pickBy } from 'lodash-es'
import { importBlockDefinitions } from './block_importer.js'
import renderTemplate from './template_renderer.js'
import renderObject from './object_renderer.js'
export default async () => {
// grab the value at the "mutator" key from all block defs, removing nulls
const mutators = pickBy(mapValues(await importBlockDefinitions(), "mutator"))
// write the javascript into a string
const renderedMutators = `const allBlockMutators = ${ renderObject(mutators) }`
// render the mutators template and return the output
return renderTemplate(renderedMutators, './src/importer/mutators.template.js')
}

View file

@ -1,89 +0,0 @@
import { compact, forOwn, keys, isString, isFunction, isArray, isNumber, isNull, isObject, isUndefined, map, isRegExp, sortBy } from 'lodash-es'
const
TAB = " ",
STARTS_WITH_NUM = /^[0-9]/,
CONTAINS_NON_ALPHANUM = /[^a-zA-Z0-9_]+/,
CONTAINS_UNESCAPED_QUOTE = /(^|[^\\])(\")/g
const quotedKey = key =>
STARTS_WITH_NUM.test(key) || CONTAINS_NON_ALPHANUM.test(key)
? `"${key}"`
: key
const renderValue = (value, tab=TAB) => {
if (isString(value)) {
return renderString(value)
} else if (isRegExp(value) || isNull(value) || isNumber(value) || isUndefined(value) || value === false) {
return value
} else if (isFunction(value)) {
return renderFunction(value, tab)
} else if (isArray(value)) {
return `[ ${value.map(i => renderValue(i, tab+TAB)).join(", ")} ]`
} else if (isObject(value)) {
const lines = []
forOwn(value, (val, key) => {
lines.push(`${tab}${quotedKey(key)}: ${renderValue(val, tab+TAB)}`)
})
return `{\n${lines.join(",\n\n")}\n${tab.slice(0, -TAB.length)}}`
} else {
// TODO: what to really do here? maybe caller passes a strategy for missing values
// return '{}'
throw new Error(`Unexpected value type: ${value}`)
}
}
const renderString = stringValue => {
// ensure double-quotes are escaped
if(CONTAINS_UNESCAPED_QUOTE.test(stringValue)) {
stringValue = stringValue.replaceAll(CONTAINS_UNESCAPED_QUOTE, "$1\\$2")
}
return `"${stringValue}"`
}
const renderFunction = (func, indentation=TAB) => {
const
functionString = func.toString(),
// capture whitespace after first newline
match = /\n\s*/.exec(functionString)?.[0].slice(1)
// early out if no newlines in function
if(!match) { return functionString }
const reIndentedFunction = functionString
// regex replace \n[measured whitespace] \n[indentation]
.replaceAll(`\n${match}`, `\n${indentation}`)
// replace last line with 2 less indentation and a closing bracket
.replace(/\n.*$/, `\n${indentation.slice(0, -2)}}`)
return reIndentedFunction
}
const renderObject = object => {
const
sortedKeys = sortBy(keys(object)),
sortedKeyValues = compact(map(sortedKeys, rawKey => {
const
key = quotedKey(rawKey),
value = object[rawKey]
return isUndefined(value)
? null
: `${key}: ${renderValue(value, TAB + TAB)}`
}))
return [
'{',
`${TAB}${sortedKeyValues.join(`,\n\n${TAB}`)}`,
'}'
].join("\n")
}
export default renderObject

View file

@ -1,13 +0,0 @@
import { mapValues } from 'lodash-es'
import { importBlockDefinitions } from './block_importer.js'
import renderTemplate from './template_renderer.js'
import renderObject from './object_renderer.js'
export default async () => {
const
blockRegenerators = mapValues(await importBlockDefinitions(), "regenerators"),
renderedRegenerators = `const blockRegenerators = ${ renderObject(blockRegenerators) }`
return renderTemplate(renderedRegenerators, './src/importer/regenerators.template.js')
}

View file

@ -1,11 +0,0 @@
import fs from 'fs'
export const renderTemplate = (renderedContent, sourceFilename) => {
// read in file contents
const existingFileContent = fs.readFileSync(sourceFilename).toString()
// return transformed contents
return existingFileContent.replace(/\/\* LOCAL->>[\s\S]*?<<-LOCAL \*\//, renderedContent)
}
export default renderTemplate

View file

@ -1,272 +0,0 @@
import { compact, forEach, identity, isEmpty, isString, keyBy, keys, map, mapValues, filter, flatMap, pickBy, reduce, without } from 'lodash-es'
import toolboxConfig from '../../app/toolbox/index.js'
import { importBlockDefinitions } from './block_importer.js'
import renderTemplate from './template_renderer.js'
import renderObject from './object_renderer.js'
const
DEBUG = false,
WARN = true,
log = (...messages) => DEBUG && console.log(...messages),
warn = (...messages) => (DEBUG || WARN) && console.warn(...messages),
error = (...messages) => DEBUG && console.error(...messages)
export const exportToolboxJSON = toolboxDef => {
return {
kind: 'categoryToolbox',
contents: generateToolboxContents(toolboxDef.contents)
}
}
let
blockDefinitionsByType = {},
blockDefinitionsByCategory = {}
const
importToolbox = async () => {
blockDefinitionsByType = await importBlockDefinitions()
blockDefinitionsByCategory = reduce(filter(blockDefinitionsByType, "toolbox"), (collection, definition) => {
const category = definition.toolbox.category
if(!collection[category]) {
collection[category] = []
}
collection[category].push(definition)
return collection
}, {})
log(`Processing Toolbox (DEBUG=true)`)
return buildToolbox()
},
buildToolbox = () => ({
kind: 'categoryToolbox',
contents: generateToolboxContents(toolboxConfig)
}),
generateToolboxContents = toolboxContents => map(toolboxContents, category => {
if(category.name) {
return generateCategoryFromDefinition(category)
}
return category
}),
generateCategoryFromDefinition = category => {
log(`- "${category.name}"`)
validateCategoryDefinition(category)
const
contents = generateCategoryContents(category),
contentKinds = map(contents, "kind"),
blockCount = filter(contentKinds, kind => kind == "block").length,
labelCount = filter(contentKinds, kind => kind == "label").length
log(` - ${blockCount} blocks added`)
if(labelCount) {
log(` - ${labelCount} labels added`)
}
if(!contents.length && !category.callback) {
warn(` - Warning: no blocks generated for category without callback "${category.name}"!`)
}
// inject other kinds of toolbox objects here
return {
kind: 'category',
name: category.name,
colour: (category.colour === 0) ? "0" : category.colour,
...{
custom: category.callback ? category.name : undefined
},
contents
}
},
EXPECTED_TOOLBOX_KEYS = [ "name", "colour", "label", "contents", "callback", "usesBlocks" ],
validateCategoryDefinition = definition => {
const
categoryKeys = keys(definition),
unexpectedKeys = without(categoryKeys, ...EXPECTED_TOOLBOX_KEYS)
if(unexpectedKeys.length) {
warn(` - Warning: Unexpected toolbox definition keys: "${unexpectedKeys.join(", ")}"`)
}
// callback precludes contents and label
if(categoryKeys.includes("callback")) {
if(categoryKeys.includes("contents")) {
warn(` - Warning: "contents" defined (${definition.contents}) on toolbox category ("${definition.name}") with a "callback" defined.`)
}
if(categoryKeys.includes("label")) {
warn(` - Warning: "contents" defined on toolbox category ("${definition.name}") with a "label" defined.`)
}
}
},
generateCategoryContents = ({ name, label, contents }) => {
const toolboxContents = []
// prepend category label(s)
if(label) {
const labelArray = isString(label) ? [label] : label
toolboxContents.push(...map(labelArray, makeLabel))
}
// if a category has a contents array, use that
if(contents) {
log(` - using toolbox def`)
const definitionContents = typeof contents[0] === "string"
? map(contents, findBlockByType)
: contents
// const blockDefinitions = map(contents, findBlockByType)
toolboxContents.push(...flatMap(definitionContents, blockDefinition => blockToLabelAndBlock(blockDefinition)))
warnOnExtraBlocksSpecifyCategory(name, definitionContents)
// otherwise add blocks by their toolbox.category
} else {
log(` - using block def`)
toolboxContents.push(...flatMap(blockDefinitionsByCategory[name] || [], blockToLabelAndBlock))
}
return toolboxContents
},
warnOnExtraBlocksSpecifyCategory = (categoryName, usedBlocks) => {
const blocksToWarn = without(blockDefinitionsByCategory[categoryName], ...usedBlocks)
if(blocksToWarn.length) {
warn(` - Warning: toolbox specifications ignored for blocks: "${map(blocksToWarn, 'type').join('", "')}"`)
}
},
findBlockByType = type => {
const blockDefinition = blockDefinitionsByType[type]
if(!blockDefinition) {
throw new Error(`No block found with type "${type}".`)
}
return blockDefinition
},
blockToLabelAndBlock = block => compact([
{
kind: 'block',
type: block.type,
inputs: blockToInputs(block),
fields: blockToFields(block)
}, block.toolbox?.label
? makeLabel(block.toolbox.label)
: null
]),
makeLabel = text => ({ kind: 'label', text }),
blockToInputs = ({ lines, inputs }) => {
if(lines) {
const inputValues =
mapValues(
keyBy(
filter(
map(lines, '[1]'),
"inputValue"),
"inputValue"),
definitionPropsToInputs)
return isEmpty(inputValues) ? undefined : inputValues
}
if(inputs) {
const inputValues = mapValues(inputs, definitionPropsToInputs)
return isEmpty(inputValues) ? undefined : inputValues
}
},
// produces:
// {
// FIELD_NAME: field_value,
// ...
// }
blockToFields = ({ lines, fields }) => {
if(lines) {
// get every field that contains a "value" property
const defaultFields =
reduce(
map(
filter(
map(lines, '[1]'),
"fields"),
"fields"),
(acc, fields) => {
forEach(fields, (field, fieldKey) => {
if(field.value){
acc[fieldKey] = field.value
}
})
return acc
}, {})
return isEmpty(defaultFields) ? undefined : defaultFields
}
if(fields) {
const defaultFields = pickBy(mapValues(fields, "value"), identity)
return isEmpty(defaultFields) ? undefined : defaultFields
}
},
definitionPropsToInputs = ({ inputValue, block, shadow }) => {
if(!block && !shadow) {
console.warn("Warning: no block or shadow specified for", inputValue)
return
}
if(block) {
const
blockJson = blockToInput(block),
shadowJson = shadowToInput(shadow || block)
return {
...blockJson,
...shadowJson
}
} else if(shadow) {
return shadowToInput(shadow)
}
},
blockToInput = block => isString(block) // is shorthand?
? { block: { type: block }} // expand to full object
: { block }, // set as shadow value
shadowToInput = shadow => isString(shadow) // is shorthand?
? { shadow: { type: shadow }} // expand to full object
: { shadow } // set as shadow value
export default importToolbox
export const importToolboxJs = () => {
const
categoriesWithCallbacks = filter(toolboxConfig, "callback"),
// { "Category Name": () -> { /* category callback */ }}
categoriesObject = mapValues(keyBy(categoriesWithCallbacks, "name"), "callback"),
renderedCategoryCallbacks = `const categoryCallbacks = ${ renderObject(categoriesObject) }`
return renderTemplate(renderedCategoryCallbacks, './src/importer/toolbox.template.js')
}

View file

@ -1,49 +0,0 @@
import { find, keys, map } from 'lodash-es'
import { importBlockJson } from './block_importer.js'
import importToolboxJson from './toolbox_importer.js'
import importWorkspaceJson from './workspace_importer.js'
import importBlocklyJs from './blockly_importer.js'
const PROCESSORS = {
"/blocks.json": async () => JSON.stringify(await importBlockJson(), null, 2),
"/toolbox.json": async () => JSON.stringify(await importToolboxJson(), null, 2),
"/workspace.json": async () => JSON.stringify(await importWorkspaceJson(), null, 2),
"/blockly_app.js": importBlocklyJs,
}
const PROCESSED_FILES = keys(PROCESSORS)
const findAnyProcessor = id => find(PROCESSORS, (_, fileEnding) => id.endsWith(fileEnding))
const prependVirtual = id => findAnyProcessor(id) && `\0${id}`
export default function ImportUserAppPlugin() {
return {
name: 'import-app-files',
resolveId(id) { return prependVirtual(id) },
load(id) {
const processor = findAnyProcessor(id)
if(!processor) { return }
return processor()
},
handleHotUpdate(ctx) {
if(ctx.file.includes("/test/")) { return }
if(!ctx.file.includes('/app/') && !ctx.file.includes('/blockly_api.js')) {
return
}
const mods = map(PROCESSED_FILES, file =>
ctx.server.moduleGraph.getModuleById(`\0.${file}`))
return ctx.modules.concat(mods)
}
}
}

View file

@ -1,6 +0,0 @@
import fs from 'fs'
export default () => {
return JSON.parse(fs.readFileSync('./app/workspace/workspace.json'))
}

View file

@ -1,16 +1,6 @@
import { defineConfig, loadEnv } from 'vite'
import ImportUserAppPlugin from './src/importer/vite_plugin.js'
import { defineConfig } from 'vite'
export default defineConfig(({ mode }) => {
const
env = loadEnv(mode, process.cwd(), ''),
plugins = env.NODE_ENV == 'development'
? [ ImportUserAppPlugin() ]
: []
return {
plugins
}
export default defineConfig(() => {
return {}
})