1
0
Fork 0

feat: Added studio to monorepo

This commit is contained in:
joostdecock 2025-04-21 18:20:52 +02:00 committed by Joost De Cock
parent c17b58f141
commit 02f841c570
177 changed files with 2467 additions and 2118 deletions

View file

@ -6,188 +6,163 @@ import { banner } from './banner.mjs'
import mustache from 'mustache'
import { execSync } from 'child_process'
import languages from '../config/languages.json' assert { type: 'json' }
// Software
import designs from '../config/software/designs.json' assert { type: 'json' }
import plugins from '../config/software/plugins.json' assert { type: 'json' }
import { getDesigns, getPlugins } from './software.mjs'
const type = process.argv[2]
const designs = await getDesigns()
const plugins = await getPlugins()
// Add new design
if (type === 'design') {
console.clear()
console.log(banner)
addDesign()
/*
* Ask input about what the user wants
*/
export const getInput = async () => {
let type = false
let template = false
let name = false
let finalName = false
const cwd = process.cwd()
// while we're not finalized on a name
while (finalName === false) {
// request type
type = (
await prompts({
type: 'select',
name: 'type',
message: ' Would you like to add a new design, or a new plugin?',
choices: [
{
title: 'Add a new FreeSewing Design',
value: 'design',
description: 'Add a new design',
},
{
title: 'Add a new FreeSewing Plugin',
value: 'plugin',
description: 'Add a new plugin',
},
],
})
).type
// If they Ctrl-C'd out of the prompt, exit here
if (!type) process.exit()
// request a name
name = (
await prompts({
type: 'text',
name: 'name',
message: ` Give a name for your new ${type}. Please stick to [a-z] only. 🏷️ `,
initial: type === 'plugin' ? 'coffee' : 'xiaomao',
})
).name
// check whether a folder with that name already exists
const dest = path.join(cwd, type + 's', type === 'plugin' ? `plugin-${name}` : name)
try {
const dir = await opendir(dest)
dir.close()
} catch {
// the folder didn't exist, so we're good to go
finalName = true
break
}
// the folder did exist, bail out
const { nextStep } = await prompts({
type: 'select',
name: 'nextStep',
message: 'It looks like that folder already exists. What should we do?',
choices: [
{ title: 'Go back', value: 'rename', description: 'Choose a different name' },
{
title: 'Exit',
value: 'exit',
description: 'Exit here so you can investigate',
},
],
})
// if they said rename, we loop again. Otherwise, we exit
if (nextStep !== 'rename') process.exit()
}
// request a template
if (type === 'design')
template = (
await prompts({
type: 'select',
name: 'template',
message: ' What template would you like to start from?',
choices: [
{ title: 'Create a design from scratch', value: 'base' },
{ title: 'Extend the Brian block (flat-sleeve block for menswear)', value: 'brian' },
{ title: 'Extend the Bent block (two-part-sleeve block for menswear)', value: 'bent' },
{ title: 'Extend the Bella block (womenswear bodice block)', value: 'bella' },
{ title: 'Extend the Breanna block (womenswear bodice block)', value: 'breanna' },
{ title: 'Extend the Titan block (unisex trouser block)', value: 'titan' },
],
initial: 0,
})
).template
return { type, name: name.toLowerCase(), template }
}
// Add new plugin
else if (type === 'plugin') {
console.clear()
console.log(banner)
addPlugin()
} else
console.log(`
Usage:
${chalk.bold.blue('npm run new design')} 👉 Adds a new design
${chalk.bold.blue('npm run new plugin')} 👉 Adds a new plugin
${chalk.bold.blue('npm run new')} ${chalk.yellow('[anything else]')} 👉 Shows this help
`)
async function addDesign() {
console.log(`
${chalk.bold.yellow('👕 Add a new design')}
${chalk.gray('≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡')}
We're going to add a new design to this repository. That's awesome 🎉
Let's start by picking a name. Naming things is hard 😬
We'd appreciate if you pick:
- a firstname like ${chalk.green('alex')}, ${chalk.green('jordan')}, ${chalk.green(
'ezra'
)}, or ${chalk.green('logan')}
- that is an aliteration with the kind of design, like ${chalk.green(
'wahid'
)} for a ${chalk.green('w')}aistcoat
Bonus points for picking a name that embraces diversity 🌈 🏾
`)
const { name } = await prompts({
type: 'text',
name: 'name',
message: 'What name would you like the design to have? ([a-z0-9_] only)',
validate: validateDesignName,
})
if (name) {
console.log('\n' + ` Alright, let's add ${chalk.green(name)} 🪄`)
createDesign(name)
async function addDesign({ name, template }) {
if (name && template) {
const valid = validateDesignName(name)
if (valid !== true) {
console.log(valid)
process.exit()
}
createDesign(name, template)
execSync('npm run reconfigure')
console.log(` Installing & linking dependencies...`)
execSync('npm install')
console.log(` All done 🎉`)
try {
console.log(`
${chalk.bold.yellow('✨ Summary')}
${chalk.gray('≡≡≡≡≡≡≡≡≡≡')}
console.log(`
👉 We've created your design skeleton at ${chalk.green('designs/' + name)}
👉 We've configured the packages via the ${chalk.green('package.json')} file
👉 We've added ${chalk.green('designs/' + name)} to the local repository
👉 We've added ${chalk.green(name)} to the FreeSewing collection
${chalk.bold.yellow('✏️ Make it your own')}
${chalk.gray('≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡')}
Hhere's a few other things you can configure:
👉 ${chalk.yellow('Author')}: Credit where credit is due; Add yourself as author in ${chalk.green(
'config/exceptions.yaml'
)}
👉 ${chalk.yellow('Description')}: We used placeholder metadata; Update it in ${chalk.green(
'config/software/designs.json'
)}
👉 ${chalk.yellow(
'Dependencies'
)}: If you need additional plugins or patterns to extend, update ${chalk.green(
🚧 We used placeholder metadata; Update it in ${chalk.green('designs/' + name + '/about.json')}
📦 If you need additional plugins or patterns to extend, update ${chalk.green(
'config/dependencies.yaml'
)}
If you change any of these, run ${chalk.blue('npm run reconfigure')} to update the package(s).
${chalk.bold.yellow('👷 Get to work')}
${chalk.gray('≡≡≡≡≡≡≡≡≡≡≡≡≡≡')}
🚀 You can now start the org development environment with ${chalk.blue('npm run org')}
🚀 You can now start the studio with ${chalk.blue('npm run studio')}
📖 Documentation is available at ${chalk.green('https://freesewing.dev/')}
🤓 Happy hacking
`)
} catch (err) {
console.log(err)
}
}
}
async function addPlugin() {
console.log(`
${chalk.bold.yellow('👕 Add a new plugin')}
${chalk.gray('≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡')}
We're going to add a new plugin to this repository. That's awesome 🎉
Let's start by picking the name for this plugin 🏷
Try to keep it to one word that explains what the plugin does e.g. ${chalk.green(
'flip'
)}, ${chalk.green('mirror')},
${chalk.green('round')}.
`)
const { name } = await prompts({
type: 'text',
name: 'name',
message: 'What name would you like the plugin to have? ([a-z] only)',
validate: validatePluginName,
})
async function addPlugin({ name }) {
if (name) {
console.log('\n' + ` Alright, let's add ${chalk.green(name)} to plugins 🪄`)
const valid = validatePluginName(name)
if (valid !== true) {
console.log(valid)
process.exit()
}
createPlugin(name)
execSync('npm run reconfigure')
console.log(` All done 🎉`)
try {
console.log(`
${chalk.bold.yellow('✨ Summary')}
${chalk.gray('≡≡≡≡≡≡≡≡≡≡')}
console.log(`
👉 We've created your plugin skeleton at ${chalk.green('plugins/plugin-' + name)}
👉 We've configured the packages via the ${chalk.green('package.json')} file
${chalk.bold.yellow('✏️ Make it your own')}
${chalk.gray('≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡')}
Hhere's a few other things you can configure:
👉 ${chalk.yellow('Author')}: Credit where credit is due; Add yourself as author in ${chalk.green(
'config/exceptions.yaml'
🚧 We used a placeholder description; Update it in ${chalk.green(
'plugins/plugin-' + name + '/about.json'
)}
👉 ${chalk.yellow('Description')}: We used a placeholder description; Update it in ${chalk.green(
'config/software/plugins.json'
)}
👉 ${chalk.yellow(
'Dependencies'
)}: If you need additional plugins or patterns to extend, update ${chalk.green(
'config/dependencies.yaml'
👷 To make your plugin do something awesome, edit ${chalk.green(
'plugins/plugin-' + name + '/src/index.mjs'
)}
If you change any of these, run ${chalk.blue('npm run reconfigure')} to update the package(s).
${chalk.bold.yellow('👷 Get to work')}
${chalk.gray('≡≡≡≡≡≡≡≡≡≡≡≡≡≡')}
🛠 You can now start the org development environment with ${chalk.blue('npm run org')}
📖 Documentation is available at ${chalk.green('https://freesewing.dev/')}
🤓 Happy hacking
`)
} catch (err) {
console.log(err)
}
}
}
function validateDesignName(name) {
if (Object.keys(designs).indexOf(name) !== -1)
if (Object.keys(designs).includes(name.toLowerCase()))
return `Sorry but ${name} is already taken so you'll need to pick something else`
if (/^([a-z][a-z0-9_]*)$/.test(name)) return true
@ -197,7 +172,7 @@ function validateDesignName(name) {
function validatePluginName(name) {
const pluginName = 'plugin-' + name
if ([...Object.keys(plugins)].indexOf(pluginName) !== -1)
if (Object.keys(plugins).includes(pluginName.toLowerCase()))
return `Sorry but ${pluginName} is already taken so you'll need to pick something else`
if (/^([a-z]+)$/.test(name)) return true
@ -221,7 +196,7 @@ function createDesign(name) {
tags: ['tagname'],
techniques: ['techname'],
}
write(['config', 'software', 'designs.json'], JSON.stringify(orderDesigns(designs), null, 2))
//write(['config', 'software', 'designs.json'], JSON.stringify(orderDesigns(designs), null, 2))
// Create folders
mkdir([...design, 'src'])
@ -275,8 +250,8 @@ function createPlugin(name) {
description,
})
plugins[pluginName] = description
write(['config', 'software', 'plugins.json'], JSON.stringify(orderPlugins(plugins), null, 2))
//Create about.json
templateOut([...template, 'about.json.mustache'], [...plugin, 'about.json'], { name })
// Create index.mjs
templateOut([...template, 'src', 'index.mjs.mustache'], [...plugin, 'src', 'index.mjs'], {
@ -349,3 +324,11 @@ function orderPlugins(plugins) {
return newPlugins
}
// Say hi, then prompt for input
console.log(banner, '\n\n')
const input = await getInput()
// Add new design
if (input.type === 'design') addDesign(input)
if (input.type === 'plugin') addPlugin(input)

253
scripts/fs.mjs Normal file
View file

@ -0,0 +1,253 @@
/*
* Various helper methods to handle file system access
*/
import fs from 'fs'
import path from 'path'
import { glob } from 'glob'
/**
* Re-export these
*/
export { fs, path, glob }
/**
* The monorepo root folder
*/
export const root = path.resolve(path.basename(import.meta.url), '..')
/**
* Copies a file
*
* @param {arrau} src - Source file
* @param {array} dst - Destination file
* @param {object} options - Options for the fs.cp call in NodeJS
*
*/
export async function cp(src, dst, options = {}) {
if (!Array.isArray(src)) src = [src]
if (!Array.isArray(dst)) dst = [dst]
try {
await fs.promises.cp(path.resolve(root, ...src), path.resolve(root, ...dst), options)
} catch (err) {
return false
}
return true
}
/**
* Removes a file
*
* @param {array} file - Path to the file to remove
* @param {object} options - Options for NodeJS' rm method
*
*/
export async function rm(file, options = { force: true }) {
if (!Array.isArray(file)) file = [file]
try {
await fs.promises.rm(path.resolve(root, ...file), options)
} catch (err) {
return false
}
return true
}
/**
* Reads a folder from disk with an optional glob pattern
*
* @param {string} (relative) path to the file to read
* @param {funtion} onError - a method to call on error
*
* @return {string} File contents, or false in case of trouble
*/
export async function globDir(
folderPath, // The (relative) path to the folder
pattern = '**/*' // Glob pattern to match
) {
if (!Array.isArray(folderPath)) folderPath = [folderPath]
let list = []
try {
list = await glob(path.resolve(root, ...folderPath) + '/' + pattern)
} catch (err) {
if (err) console.log(err)
return false
}
return list
}
/**
* Creates a directory/folder
*
* @param {string} dirPath - (relative) path to the folder to create
* @param {funtion} onError - a method to call on error
*
* @return {string} File contents, or false in case of trouble
*/
export async function mkdir(
dirPath, // The (relative) path to the folder to create
onError // Method to run on error
) {
if (!Array.isArray(dirPath)) dirPath = [dirPath]
let dir
try {
dir = path.resolve(root, ...dirPath)
await fs.promises.mkdir(dir, { recursive: true })
} catch (err) {
if (onError) onError(err)
return false
}
return true
}
/**
* Reads a file from disk
*
* @param {string} (relative) path to the file to read
* @param {funtion} onError - a method to call on error
*
* @return {string} File contents, or false in case of trouble
*/
export async function readFile(
filePath, // The (relative) path to the file
onError, // Method to run on error
binary = false
) {
if (!Array.isArray(filePath)) filePath = [filePath]
let content, file
try {
file = path.resolve(root, ...filePath)
content = await fs.promises.readFile(file, binary ? undefined : 'utf-8')
} catch (err) {
if (onError) onError(err)
return false
}
return content
}
/**
* Reads a JSON file from disk and parses it
*
* @param {string} path - (relative) path to the file to read
* @param {string} onError - a string to log on error rather than the default
*
* @return {string} File contents, or false in case of trouble
*/
export async function readJsonFile(
filePath, // The (relative) path to the file
onError // Method to run on error
) {
if (!Array.isArray(filePath)) filePath = [filePath]
let content
try {
content = await readFile(path.join(root, ...filePath), onError, true)
content = JSON.parse(content)
} catch (err) {
if (onError) onError(err)
return false
}
return content
}
/**
* Writes a file to disk
*
* @param {string} filePath - (relative) path to the file to write
* @param {string} data - the data to write to disk
* @param {function} log - a logger instance (or false)
* @param {octal} mode - a mode for chmod
*
* @return {bool} true of success, false in case of trouble
*/
export async function writeFile(
filePath, // The (relative) path to the file
data, // The data to write to disk
log = false,
mode = 0o666
) {
if (!Array.isArray(filePath)) filePath = [filePath]
let file
try {
file = path.resolve(root, ...filePath)
await fs.promises.mkdir(path.dirname(file), { recursive: true })
await fs.promises.writeFile(file, data)
await fs.promises.chmod(file, mode)
} catch (err) {
if (log) log.warn(err, `Failed to write file: ${file}`)
else console.log(`Failed to write file: ${file}`)
return false
}
return true
}
/**
* Writes a JSON file to disk
*
* @param {string} filePath - (relative) path to the file to write
* @param {string} data - the data to write to disk as a Javascript object
*
* @return {bool} true of success, false in case of trouble
*/
export async function writeJsonFile(filePath, data, log, mode) {
return await writeFile(filePath, JSON.stringify(data, null, 2), log, mode)
}
/**
* Reads the contents of a directory (non-recursive)
*
* @param {string} dirPath - (relative) path to the directory to read
* @param {funtion} onError - a method to call on error
*/
export async function readDirectory(dirPath, onError) {
if (!Array.isArray(dirPath)) dirPath = [dirPath]
let files
try {
const dir = path.resolve(root, ...dirPath)
files = await fs.promises.readdir(dir)
} catch (err) {
if (onError) onError(err)
return false
}
return files
}
/**
* Copies a folder recursively
*
* @param {string} srcDir - The source folder to copy
* @param {string} dstDir - The destination folder
*/
export async function copyFolderRecursively(srcDir, dstDir) {
/*
* Ensure target folder exists
*/
await mkdir(dstDir)
/*
* Glob all files to copy
* Generate relative from and to arrays
*/
const files = (await globDir(srcDir, '**/*'))
.sort()
.map((target) => target.split(root).pop().slice(1).split('/'))
.map((target) => ({
from: target,
to: [...dstDir, ...target.slice(srcDir.length)],
}))
/*
* Copy files, create folders
*/
for (const op of files) {
const stat = fs.statSync(path.join(root, ...op.from))
if (stat.isDirectory()) await mkdir(op.to)
else await cp(op.from, op.to)
}
}

View file

@ -1,18 +1,28 @@
import path from 'path'
import fs from 'fs'
import { glob } from 'glob'
//import path from 'path'
//import fs from 'fs'
import {
fs,
cp,
readFile,
writeFile,
path,
glob,
copyFolderRecursively,
root,
readJsonFile,
writeJsonFile,
} from './fs.mjs'
//import { glob } from 'glob'
import yaml from 'js-yaml'
import chalk from 'chalk'
import mustache from 'mustache'
import conf from '../lerna.json' assert { type: 'json' }
const { version } = conf
import { software, publishedTypes as types, plugins } from '../config/software/index.mjs'
import { getSoftware } from './software.mjs'
//import { software, publishedTypes as types, plugins } from '../config/software/index.mjs'
import { collection } from '@freesewing/collection'
import { capitalize } from '../packages/utils/src/index.mjs'
// Working directory
const cwd = process.cwd()
/*
* When we're building a site (on Netlify for example) SITEBUILD
* will be set and we'll do things differently to speed up the build.
@ -26,7 +36,7 @@ if (SITEBUILD) console.log('Site build | Configure monorepo accordingly')
* This object holds info about the repository
*/
const repo = {
path: cwd,
path: root,
defaults: readConfigFile('defaults.yaml'),
keywords: readConfigFile('keywords.yaml'),
badges: SITEBUILD ? null : readConfigFile('badges.yaml'),
@ -41,13 +51,17 @@ const repo = {
readme: SITEBUILD ? null : readTemplateFile('readme.dflt.md'),
pluginTests: SITEBUILD ? null : readTemplateFile('plugin.test.mjs'),
designTests: SITEBUILD ? null : readTemplateFile('design.test.mjs.mustache'),
data: SITEBUILD ? null : readTemplateFile('data.dflt.mjs.mustache'),
collection: {
pkg: readTemplateFile('collection-pkg.mustache'),
hook: readTemplateFile('collection-hook.mustache'),
},
},
dirs: foldersByType(),
contributors: SITEBUILD ? null : fs.readFileSync(path.join(cwd, 'CONTRIBUTORS.md'), 'utf-8'),
contributors: SITEBUILD ? null : fs.readFileSync(path.join(root, 'CONTRIBUTORS.md'), 'utf-8'),
ac: SITEBUILD
? null
: JSON.parse(fs.readFileSync(path.join(cwd, '.all-contributorsrc'), 'utf-8')),
: JSON.parse(fs.readFileSync(path.join(root, '.all-contributorsrc'), 'utf-8')),
software: await getSoftware(),
hiddenDesigns: ['examples', 'legend', 'plugintest', 'rendertest', 'magde'],
}
/*
@ -56,25 +70,64 @@ const repo = {
const log = process.stdout
// Step 0: Avoid symlink so Windows users don't complain
const copyThese = [
const cpFolders = [
{
from: ['scripts', 'banner.mjs'],
to: ['packages', 'new-design', 'lib', 'banner.mjs'],
from: ['sites', 'org', 'plugins'],
to: ['sites', 'studio', 'plugins'],
},
{
from: ['packages', 'studio', 'template', 'docs'],
to: ['sites', 'studio', 'docs'],
},
{
from: ['packages', 'studio', 'template', 'src'],
to: ['sites', 'studio', 'src'],
},
{
from: ['packages', 'studio', 'template', 'static'],
to: ['sites', 'studio', 'static'],
},
{
from: ['packages', 'studio', 'template', 'scripts'],
to: ['sites', 'studio', 'scripts'],
},
{
from: ['sites', 'org', 'src', 'css'],
to: ['sites', 'studio', 'src', 'css'],
},
]
for (const cp of copyThese) {
fs.copyFile(path.join(repo.path, ...cp.from), path.join(repo.path, ...cp.to), () => null)
}
const cpFiles = [
{
from: ['sites', 'org', 'babel.config.mjs'],
to: ['sites', 'studio', 'babel.config.mjs'],
},
{
from: ['sites', 'org', 'postcss.config.js'],
to: ['sites', 'studio', 'postcss.config.js'],
},
{
from: ['sites', 'org', 'plugins'],
to: ['sites', 'studio', 'plugins'],
},
{
from: ['sites', 'org', 'src', 'pages', 'style.mjs'],
to: ['sites', 'studio', 'src', 'pages', 'style.mjs'],
},
{
from: ['sites', 'studio', 'add.mdx'],
to: ['sites', 'studio', 'docs', 'add.mdx'],
},
]
for (const op of cpFolders) await copyFolderRecursively(op.from, op.to)
for (const op of cpFiles) await cp(op.from, op.to)
// Step 1: Generate main README file from template
if (!SITEBUILD) {
log.write(chalk.blueBright('Generating out main README file...'))
fs.writeFileSync(
path.join(repo.path, 'README.md'),
mustache.render(
fs.readFileSync(path.join(repo.path, 'config', 'templates', 'readme.main.md'), 'utf-8'),
{ allcontributors: repo.ac.contributors.length }
) + repo.contributors
log.write(chalk.blueBright('Templating out main README file...'))
const template = await readFile(['config', 'templates', 'readme.main.md'])
await writeFile(
'README.md',
mustache.render(template, { allcontributors: repo.ac.contributors.length }) + repo.contributors
)
log.write(chalk.green(' Done\n'))
}
@ -87,67 +140,67 @@ if (!SITEBUILD) {
// Step 3: Generate package.json, README, and CHANGELOG
log.write(chalk.blueBright('Generating package-specific files...'))
for (const pkg of Object.values(software)) {
fs.writeFileSync(
path.join(cwd, pkg.folder, pkg.name, 'package.json'),
JSON.stringify(packageJson(pkg), null, 2) + '\n'
)
if (!SITEBUILD) {
if (pkg.type !== 'site') {
fs.writeFileSync(path.join(cwd, pkg.folder, pkg.name, 'README.md'), readme(pkg))
fs.writeFileSync(path.join(cwd, pkg.folder, pkg.name, 'CHANGELOG.md'), changelog(pkg))
if ([...collection, 'bonny'].includes(pkg.name)) {
const aboutFile = path.join(cwd, 'designs', pkg.name, 'about.json')
const about = JSON.parse(fs.readFileSync(aboutFile, 'utf-8'))
about.version = version
about.pkg = `@freesewing/${about.id}`
fs.writeFileSync(aboutFile, JSON.stringify(about, null, 2))
}
for (const type of ['designs', 'packages', 'plugins']) {
for (const folder of Object.keys(repo.software[type])) {
const about = await readJsonFile([type, folder, 'about.json'])
await writeJsonFile([type, folder, 'package.json'], packageJson(folder, type, about))
if (!SITEBUILD) {
await writeFile([type, folder, 'README.md'], readme(folder, type, about))
await writeFile([type, folder, 'CHANGELOG.md'], changelog(folder, type, about))
await writeJsonFile([type, folder, 'about.json'], { ...about, version })
}
}
}
log.write(chalk.green(' Done\n'))
// Step 4: Generate overall CHANGELOG.md
if (!SITEBUILD) fs.writeFileSync(path.join(repo.path, 'CHANGELOG.md'), changelog('global'))
if (!SITEBUILD) await writeFile('CHANGELOG.md', changelog('global'))
// Step 5: Generate tests for designs and plugins
if (!SITEBUILD) {
for (const design of collection) {
fs.writeFileSync(
path.join(repo.path, 'designs', design, 'tests', 'shared.test.mjs'),
for (const design in repo.software.designs) {
await writeFile(
['designs', design, 'tests', 'shared.test.mjs'],
mustache.render(repo.templates.designTests, { name: design, Name: capitalize(design) })
)
}
for (const plugin in plugins) {
fs.writeFileSync(
path.join(repo.path, 'plugins', plugin, 'tests', 'shared.test.mjs'),
repo.templates.pluginTests
)
for (const plugin in repo.software.plugins) {
await writeFile(['plugins', plugin, 'tests', 'shared.test.mjs'], repo.templates.pluginTests)
}
}
// Step 6: Generate collection package and hook dynamic files
const designList = Object.keys(repo.software.designs).filter(
(name) => !repo.hiddenDesigns.includes(name)
)
const designImports = designList
.map((name) => `import { ${capitalize(name)} as ${name} } from '@freesewing/${name}'`)
.join('\n')
await writeFile(
['packages', 'collection', 'src', 'index.mjs'],
mustache.render(repo.templates.collection.pkg, {
designImports,
designList: designList.join(',\n '),
})
)
await writeFile(
['packages', 'react', 'hooks', 'useDesign', 'index.mjs'],
mustache.render(repo.templates.collection.hook, {
designImports,
designList: designList.join(',\n '),
})
)
// All done
log.write(chalk.green(' All done\n'))
process.exit()
/*
* Generates a list of folders by type
*/
function foldersByType() {
const dirs = {}
for (const dir of types) {
dirs[dir] = glob.sync('*', { cwd: path.join(cwd, dir) })
}
return dirs
}
/**
* Reads a template file
*/
function readTemplateFile(file) {
return fs.readFileSync(path.join(cwd, 'config', 'templates', file), 'utf-8')
return fs.readFileSync(path.join(root, 'config', 'templates', file), 'utf-8')
}
/**
@ -156,19 +209,19 @@ function readTemplateFile(file) {
function readConfigFile(file, replace = false) {
if (replace)
return yaml.load(
mustache.render(fs.readFileSync(path.join(cwd, 'config', file), 'utf-8'), replace)
mustache.render(fs.readFileSync(path.join(root, 'config', file), 'utf-8'), replace)
)
return yaml.load(fs.readFileSync(path.join(cwd, 'config', file), 'utf-8'))
return yaml.load(fs.readFileSync(path.join(root, 'config', file), 'utf-8'))
}
/**
* Reads info.md from the package directory
* Returns its contents if it exists, or an empty string if not
*/
function readInfoFile(pkg) {
function readInfoFile(pkg, type) {
let markup = ''
try {
markup = fs.readFileSync(path.join(cwd, pkg.folder, pkg.name, 'info.md'), 'utf-8')
markup = fs.readFileSync(path.join(root, type, pkg, 'info.md'), 'utf-8')
} catch (err) {
return ''
}
@ -179,44 +232,36 @@ function readInfoFile(pkg) {
/**
* Returns an array of keywords for a package
*/
function keywords(pkg) {
if (pkg.type === 'site') return []
if (typeof repo.keywords[pkg.name] !== 'undefined') return repo.keywords[pkg.name]
if (typeof repo.keywords[pkg.type] !== 'undefined') return repo.keywords[pkg.type]
else {
console.log(
chalk.redBright.bold('Problem:'),
chalk.redBright(`No keywords for package ${pkg.name} which is of type ${pkg.type}`)
)
process.exit()
}
function keywords(pkg, type) {
if (typeof repo.keywords[pkg] !== 'undefined') return repo.keywords[pkg]
if (typeof repo.keywords[type] !== 'undefined') return repo.keywords[type]
if (Object.keys(repo.software.designs).includes(pkg)) return repo.keywords.design
else return repo.keywords.other
}
/**
* Returns an plain object of scripts for a package
*/
function scripts(pkg) {
function scripts(pkg, type) {
let runScripts = {}
if (pkg.type !== 'site') {
for (const key of Object.keys(repo.scripts._)) {
runScripts[key] = mustache.render(repo.scripts._[key], {
name: pkg.name,
for (const key of Object.keys(repo.scripts._)) {
runScripts[key] = mustache.render(repo.scripts._[key], {
name: pkg,
})
}
if (typeof repo.scripts._types[type] !== 'undefined') {
for (const key of Object.keys(repo.scripts._types[type])) {
runScripts[key] = mustache.render(repo.scripts._types[type][key], {
name: pkg,
})
}
}
if (typeof repo.scripts._types[pkg.type] !== 'undefined') {
for (const key of Object.keys(repo.scripts._types[pkg.type])) {
runScripts[key] = mustache.render(repo.scripts._types[pkg.type][key], {
name: pkg.name,
})
}
}
if (typeof repo.scripts[pkg.name] !== 'undefined') {
for (const key of Object.keys(repo.scripts[pkg.name])) {
if (repo.scripts[pkg.name][key] === '!') delete runScripts[key]
if (typeof repo.scripts[pkg] !== 'undefined') {
for (const key of Object.keys(repo.scripts[pkg])) {
if (repo.scripts[pkg][key] === '!') delete runScripts[key]
else
runScripts[key] = mustache.render(repo.scripts[pkg.name][key], {
name: pkg.name,
runScripts[key] = mustache.render(repo.scripts[pkg][key], {
name: pkg,
})
}
}
@ -233,16 +278,16 @@ function scripts(pkg) {
* - peer (for peerDependencies)
*
*/
function dependencies(section, pkg) {
function dependencies(section, pkg, type) {
let dependencies = {}
if (
typeof repo.dependencies._types[pkg.type] !== 'undefined' &&
typeof repo.dependencies._types[pkg.type][section] !== 'undefined'
typeof repo.dependencies._types[type] !== 'undefined' &&
typeof repo.dependencies._types[type][section] !== 'undefined'
)
dependencies = repo.dependencies._types[pkg.type][section]
if (typeof repo.dependencies[pkg.name] === 'undefined') return dependencies
if (typeof repo.dependencies[pkg.name][section] !== 'undefined')
return { ...dependencies, ...repo.dependencies[pkg.name][section] }
dependencies = repo.dependencies._types[type][section]
if (typeof repo.dependencies[pkg] === 'undefined') return dependencies
if (typeof repo.dependencies[pkg][section] !== 'undefined')
return { ...dependencies, ...repo.dependencies[pkg][section] }
return dependencies
}
@ -250,46 +295,36 @@ function dependencies(section, pkg) {
/**
* Creates a package.json file for a package
*/
function packageJson(pkg) {
function packageJson(pkg, type, about) {
let pkgConf = {}
// Let's keep these at the top
pkgConf.name = fullName(pkg.name)
pkgConf.name = fullName(about.id)
pkgConf.version = version
pkgConf.description = pkg.description
pkgConf.description = about.description
pkgConf = {
...pkgConf,
...JSON.parse(mustache.render(repo.templates.pkg, { name: pkg.name })),
...JSON.parse(mustache.render(repo.templates.pkg, { name: about.id })),
}
pkgConf.keywords = pkgConf.keywords.concat(keywords(pkg))
pkgConf.scripts = scripts(pkg)
pkgConf.keywords = pkgConf.keywords.concat(keywords(pkg, type))
pkgConf.scripts = scripts(pkg, type)
if (repo.exceptions.skipTests.indexOf(pkg.name) !== -1) {
pkgConf.scripts.test = `echo "skipping tests for ${pkg.name}"`
pkgConf.scripts.testci = `echo "skipping tests for ${pkg.name}"`
if (repo.exceptions.skipTests.includes(pkg)) {
pkgConf.scripts.test = `echo "skipping tests for ${about.id}"`
pkgConf.scripts.testci = `echo "skipping tests for ${about.id}"`
}
pkgConf.dependencies = dependencies('_', pkg)
pkgConf.devDependencies = dependencies('dev', pkg)
pkgConf.peerDependencies = dependencies('peer', pkg)
if (typeof repo.exceptions.packageJson[pkg.name] !== 'undefined') {
pkgConf.dependencies = dependencies('_', pkg, type)
pkgConf.devDependencies = dependencies('dev', pkg, type)
pkgConf.peerDependencies = dependencies('peer', pkg, type)
if (typeof repo.exceptions.packageJson[pkg] !== 'undefined') {
pkgConf = {
...pkgConf,
...repo.exceptions.packageJson[pkg.name],
...repo.exceptions.packageJson[pkg],
}
for (let key of Object.keys(repo.exceptions.packageJson[pkg.name])) {
if (repo.exceptions.packageJson[pkg.name][key] === '!') delete pkgConf[key]
for (let key of Object.keys(repo.exceptions.packageJson[pkg])) {
if (repo.exceptions.packageJson[pkg][key] === '!') delete pkgConf[key]
}
}
if (pkg.type === 'site') {
delete pkgConf.keywords
delete pkgConf.type
delete pkgConf.module
delete pkgConf.exports
delete pkgConf.files
delete pkgConf.publishConfig
pkgConf.private = true
}
return pkgConf
}
@ -325,19 +360,19 @@ function formatBadge(badge, name, fullname) {
* Returns the full (namespaced) name of a package
*/
function fullName(name) {
if (repo.exceptions.noNamespace.indexOf(name) !== -1) return name
if (repo.exceptions.noNamespace.includes(name)) return name
else return `@freesewing/${name}`
}
/**
* Creates a README.md file for a package
*/
function readme(pkg) {
function readme(pkg, type, about) {
let markup = mustache.render(repo.templates.readme, {
fullname: fullName(pkg.name),
description: pkg.description,
badges: badges(pkg.name),
info: readInfoFile(pkg),
fullname: fullName(pkg),
description: about.description,
badges: badges(pkg, type),
info: readInfoFile(pkg, type),
contributors: repo.contributors,
})
@ -349,8 +384,8 @@ function readme(pkg) {
*/
function changelog(pkg) {
let markup = mustache.render(repo.templates.changelog, {
fullname: pkg === 'global' ? 'FreeSewing (global)' : fullName(pkg.name),
changelog: pkg === 'global' ? globalChangelog() : packageChangelog(pkg.name),
fullname: pkg === 'global' ? 'FreeSewing (global)' : fullName(pkg),
changelog: pkg === 'global' ? globalChangelog() : packageChangelog(pkg),
})
return markup
@ -366,7 +401,12 @@ function globalChangelog() {
markup += '\n## ' + v
if (v !== 'Unreleased') markup += ' (' + formatDate(changes.date) + ')'
markup += '\n\n'
for (let pkg of ['global', ...Object.keys(software)]) {
for (let pkg of [
'global',
...Object.keys(repo.software.designs),
...Object.keys(repo.software.plugins),
...Object.keys(repo.software.packages),
]) {
let changed = false
for (let type of repo.changetypes) {
if (
@ -447,14 +487,7 @@ function formatDate(date) {
* Make sure we have (at least) a description for each package
*/
function validate() {
for (const type in repo.dirs) {
for (const dir of repo.dirs[type]) {
if (typeof software?.[dir]?.description !== 'string') {
log.write(chalk.redBright(` No description for package ${type}/${dir}` + '\n'))
return false
}
}
}
// Nothing to validate, perhaps we should change that
return true
}

44
scripts/software.mjs Normal file
View file

@ -0,0 +1,44 @@
import { path, globDir, readJsonFile } from './fs.mjs'
export const getDesigns = async () => {
const designs = {}
const list = await getFolders('designs')
for (const design of list) {
const data = await readJsonFile(['designs', design, 'about.json'])
designs[design] = data
}
return designs
}
export const getPackages = async () => {
const packages = {}
const list = await getFolders('packages')
for (const pkg of list) {
const data = await readJsonFile(['packages', pkg, 'about.json'])
packages[pkg] = data
}
return packages
}
export const getPlugins = async () => {
const plugins = {}
const list = await getFolders('plugins')
for (const plugin of list) {
const data = await readJsonFile(['plugins', plugin, 'about.json'])
plugins[plugin] = data
}
return plugins
}
export const getSoftware = async () => ({
designs: await getDesigns(),
packages: await getPackages(),
plugins: await getPlugins(),
})
// Helper
const getFolders = async (dir) =>
(await globDir(dir, '*')).map((file) => path.basename(file)).sort()