Adding the first glitch gallery
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
|
||||
const cwd = process.cwd()
|
||||
|
||||
function fail(message, exitCode = 1) {
|
||||
console.error(message)
|
||||
process.exit(exitCode)
|
||||
}
|
||||
|
||||
function discoverComponents() {
|
||||
return fs
|
||||
.readdirSync(cwd, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && entry.name.startsWith('glitch_'))
|
||||
.map((entry) => entry.name)
|
||||
.filter((name) => fs.existsSync(path.join(cwd, name, 'package.json')))
|
||||
.sort()
|
||||
}
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const [command = 'list', ...rest] = argv
|
||||
const options = {
|
||||
command,
|
||||
components: [],
|
||||
hostDir: '',
|
||||
stagingDir: path.join(cwd, '.glitch-release')
|
||||
}
|
||||
|
||||
for (let index = 0; index < rest.length; index += 1) {
|
||||
const arg = rest[index]
|
||||
if (arg === '--host') {
|
||||
options.hostDir = path.resolve(cwd, rest[index + 1] ?? '')
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (arg === '--staging') {
|
||||
options.stagingDir = path.resolve(cwd, rest[index + 1] ?? '')
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
options.components.push(arg)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
function resolveComponents(requested) {
|
||||
const discovered = discoverComponents()
|
||||
if (requested.length === 0) return discovered
|
||||
|
||||
const missing = requested.filter((name) => !discovered.includes(name))
|
||||
if (missing.length > 0) {
|
||||
fail(`Unknown components: ${missing.join(', ')}`)
|
||||
}
|
||||
|
||||
return requested
|
||||
}
|
||||
|
||||
function loadComponent(componentDirName) {
|
||||
const componentDir = path.join(cwd, componentDirName)
|
||||
const packagePath = path.join(componentDir, 'package.json')
|
||||
const manifestPath = path.join(componentDir, 'glitch.manifest.json')
|
||||
const packageJson = readJson(packagePath)
|
||||
const manifest = fs.existsSync(manifestPath) ? readJson(manifestPath) : null
|
||||
|
||||
return {
|
||||
componentDirName,
|
||||
componentDir,
|
||||
packagePath,
|
||||
manifestPath,
|
||||
packageJson,
|
||||
manifest
|
||||
}
|
||||
}
|
||||
|
||||
function validateComponent(component) {
|
||||
const requiredFiles = ['package.json', 'src/index.tsx', 'src/Component.tsx', 'src/types.ts', 'src/dev.tsx']
|
||||
|
||||
const errors = []
|
||||
const warnings = []
|
||||
const hasManifest = Boolean(component.manifest)
|
||||
const hasStandardSignals = hasManifest
|
||||
|
||||
for (const relativePath of requiredFiles) {
|
||||
if (!fs.existsSync(path.join(component.componentDir, relativePath))) {
|
||||
if (hasStandardSignals) {
|
||||
errors.push(`Missing required file: ${relativePath}`)
|
||||
} else {
|
||||
warnings.push(`Legacy structure is missing standard file: ${relativePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasViteConfig =
|
||||
fs.existsSync(path.join(component.componentDir, 'vite.config.ts')) ||
|
||||
fs.existsSync(path.join(component.componentDir, 'vite.config.mjs')) ||
|
||||
fs.existsSync(path.join(component.componentDir, 'vite.config.js'))
|
||||
|
||||
if (!hasViteConfig) {
|
||||
if (hasStandardSignals) {
|
||||
errors.push('Missing required file: vite.config.ts')
|
||||
} else {
|
||||
warnings.push('Legacy structure is missing standard file: vite.config.ts')
|
||||
}
|
||||
}
|
||||
|
||||
if (!component.packageJson.scripts?.dev && hasStandardSignals) {
|
||||
errors.push('Missing package script: dev')
|
||||
}
|
||||
|
||||
if (!component.packageJson.scripts?.build && hasStandardSignals) {
|
||||
errors.push('Missing package script: build')
|
||||
}
|
||||
|
||||
const indexPath = path.join(component.componentDir, 'src', 'index.tsx')
|
||||
if (fs.existsSync(indexPath)) {
|
||||
const source = fs.readFileSync(indexPath, 'utf8')
|
||||
if (!source.includes('export default') && hasStandardSignals) {
|
||||
errors.push('src/index.tsx must export default')
|
||||
}
|
||||
if (!source.includes('export const metadata') && hasStandardSignals) {
|
||||
errors.push('src/index.tsx must export metadata')
|
||||
}
|
||||
if (source.includes("import './dev.css'") || source.includes("import './dev-theme.css'")) {
|
||||
warnings.push('Production entry imports a dev-only stylesheet')
|
||||
}
|
||||
}
|
||||
|
||||
const styleFiles = ['src/styles.css', 'src/styles.module.css'].filter((relativePath) =>
|
||||
fs.existsSync(path.join(component.componentDir, relativePath))
|
||||
)
|
||||
|
||||
for (const relativePath of styleFiles) {
|
||||
const source = fs.readFileSync(path.join(component.componentDir, relativePath), 'utf8')
|
||||
if (source.includes('@import url(')) {
|
||||
warnings.push(`${relativePath} imports a remote stylesheet`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!component.manifest) {
|
||||
warnings.push('Missing glitch.manifest.json')
|
||||
} else {
|
||||
for (const key of ['componentId', 'displayName', 'version', 'folderName', 'packageName', 'entry']) {
|
||||
if (!component.manifest[key]) {
|
||||
errors.push(`glitch.manifest.json is missing "${key}"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(path.join(component.componentDir, 'README.md'))) {
|
||||
warnings.push('Missing README.md')
|
||||
}
|
||||
|
||||
return { errors, warnings }
|
||||
}
|
||||
|
||||
function runBuild(component) {
|
||||
const result = spawnSync('npm', ['run', 'build'], {
|
||||
cwd: component.componentDir,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
|
||||
if (result.status !== 0) {
|
||||
fail(`Build failed for ${component.componentDirName}`, result.status ?? 1)
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDirectory(directory) {
|
||||
fs.mkdirSync(directory, { recursive: true })
|
||||
}
|
||||
|
||||
function copyDirectoryContents(sourceDir, destinationDir) {
|
||||
ensureDirectory(destinationDir)
|
||||
for (const entry of fs.readdirSync(sourceDir)) {
|
||||
fs.cpSync(path.join(sourceDir, entry), path.join(destinationDir, entry), {
|
||||
force: true,
|
||||
recursive: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function releaseComponent(component, hostDir, stagingDir) {
|
||||
const distDir = path.join(component.componentDir, 'dist')
|
||||
if (!fs.existsSync(distDir)) {
|
||||
fail(`Missing dist directory for ${component.componentDirName}. Run build first.`)
|
||||
}
|
||||
|
||||
const manifest =
|
||||
component.manifest ??
|
||||
{
|
||||
componentId: component.componentDirName.replace(/^glitch_/, '').replaceAll('_', '-'),
|
||||
displayName: component.packageJson.description ?? component.packageJson.name,
|
||||
version: component.packageJson.version,
|
||||
folderName: component.componentDirName,
|
||||
packageName: component.packageJson.name,
|
||||
entry: `dist/${component.packageJson.main?.replace(/^\.\/dist\//, '') ?? 'index.js'}`,
|
||||
source: 'src/index.tsx',
|
||||
tags: ['legacy']
|
||||
}
|
||||
|
||||
const releaseDir = path.join(hostDir, manifest.folderName)
|
||||
copyDirectoryContents(distDir, releaseDir)
|
||||
fs.writeFileSync(path.join(releaseDir, 'glitch.manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
|
||||
const stagingComponentDir = path.join(stagingDir, manifest.folderName)
|
||||
copyDirectoryContents(distDir, stagingComponentDir)
|
||||
fs.writeFileSync(
|
||||
path.join(stagingComponentDir, 'glitch.manifest.json'),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`
|
||||
)
|
||||
}
|
||||
|
||||
const options = parseArgs(process.argv.slice(2))
|
||||
const componentNames = resolveComponents(options.components)
|
||||
const components = componentNames.map(loadComponent)
|
||||
|
||||
switch (options.command) {
|
||||
case 'list': {
|
||||
for (const componentName of componentNames) {
|
||||
console.log(componentName)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'validate': {
|
||||
let errorCount = 0
|
||||
let warningCount = 0
|
||||
|
||||
for (const component of components) {
|
||||
const { errors, warnings } = validateComponent(component)
|
||||
if (errors.length === 0 && warnings.length === 0) {
|
||||
console.log(`OK ${component.componentDirName}`)
|
||||
continue
|
||||
}
|
||||
|
||||
console.log(component.componentDirName)
|
||||
for (const error of errors) {
|
||||
errorCount += 1
|
||||
console.log(` ERROR ${error}`)
|
||||
}
|
||||
for (const warning of warnings) {
|
||||
warningCount += 1
|
||||
console.log(` WARN ${warning}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCount > 0) {
|
||||
fail(`Validation failed with ${errorCount} error(s) and ${warningCount} warning(s).`)
|
||||
}
|
||||
|
||||
console.log(`Validation passed with ${warningCount} warning(s).`)
|
||||
break
|
||||
}
|
||||
|
||||
case 'build': {
|
||||
for (const component of components) {
|
||||
console.log(`=== Building ${component.componentDirName} ===`)
|
||||
runBuild(component)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'release': {
|
||||
const hostDir =
|
||||
options.hostDir ||
|
||||
path.resolve(cwd, '..', 'gnommoweb', 'src', 'components', 'glitch')
|
||||
|
||||
ensureDirectory(hostDir)
|
||||
ensureDirectory(options.stagingDir)
|
||||
|
||||
for (const component of components) {
|
||||
console.log(`=== Releasing ${component.componentDirName} ===`)
|
||||
runBuild(component)
|
||||
releaseComponent(component, hostDir, options.stagingDir)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
fail(`Unknown command: ${options.command}`)
|
||||
}
|
||||
Reference in New Issue
Block a user