1
0
Fork 0
freesewing/scripts/add-software.mjs

284 lines
8.2 KiB
JavaScript
Raw Normal View History

2025-04-26 15:29:25 +02:00
import {
fs,
cp,
path,
globDir,
copyFolderRecursively,
2025-04-27 14:50:28 +02:00
mkdir,
2025-04-26 15:29:25 +02:00
rm,
root,
templateOut,
writeJsonFile,
} from './fs.mjs'
import prompts from 'prompts'
chore: Re-structure workspaces, enforce build order These are some changes in the way the monorepo is structured, that are aimed at making it easier to get started. There are two important changes: **Multiple workspaces** We had a yarn workspaces setup at `packages/*`. But our monorepo has grown to 92 packages which can be overwhelming for people not familiar with the package names. To remedy this, I've split it into 4 different workspaces: - `designs/*`: Holds FreeSewing designs (think patterns) - `plugins/*`: Holds FreeSewing plugins - `packages/*`: Holds other packages published on NPM - `sites/*`: Holds software that is not published as an NPM package, such as our various websites and backend API This should make it easier to find things, and to answer questions like *where do I find the code for the plugins*. **Updated reconfigure script to handle build order** One problem when bootstrapping the repo is inter-dependencies between packages. For example, building a pattern will only work once `plugin-bundle` is built. Which will only work once all plugins in the bundle or built. And that will only work when `core` is built, and so on. This can be frustrating for new users as `yarn buildall` will fail. And it gets overlooked by seasoned devs because they're likely to have every package built in their repo so this issue doesn't concern them. To remedy this, we now have a `config/build-order.mjs` file and the updated `/scripts/reconfigure.mjs` script will enforce the build order so that things *just work*.
2022-06-16 17:11:31 +02:00
import chalk from 'chalk'
import { banner } from './banner.mjs'
import { execSync } from 'child_process'
import languages from '../config/languages.json' assert { type: 'json' }
2025-04-21 18:20:52 +02:00
import { getDesigns, getPlugins } from './software.mjs'
2025-04-26 15:29:25 +02:00
import conf from '../lerna.json' assert { type: 'json' }
const { version } = conf
2025-04-21 18:20:52 +02:00
const designs = await getDesigns()
const plugins = await getPlugins()
/*
* Ask input about what the user wants
*/
export const getInput = async () => {
let type = false
let template = false
let name = false
let finalName = false
// 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
2025-04-26 15:29:25 +02:00
const dest = path.join(root, type + 's', type === 'plugin' ? `plugin-${name}` : name)
2025-04-21 18:20:52 +02:00
try {
const dir = await opendir(dest)
dir.close()
} catch {
// the folder didn't exist, so we're good to go
finalName = true
break
}
2025-04-21 18:20:52 +02:00
// 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',
},
],
})
2025-04-21 18:20:52 +02:00
// if they said rename, we loop again. Otherwise, we exit
if (nextStep !== 'rename') process.exit()
}
2025-04-21 18:20:52 +02:00
// 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 }
}
2025-04-21 18:20:52 +02:00
async function addDesign({ name, template }) {
if (name && template) {
const valid = validateDesignName(name)
if (valid !== true) {
console.log(valid)
process.exit()
}
2025-04-26 15:29:25 +02:00
await createDesign(name, template)
2022-10-02 13:11:29 -07:00
execSync('npm run reconfigure')
2025-04-26 15:29:25 +02:00
execSync('npm install')
2025-04-21 18:20:52 +02:00
console.log(`
2022-10-02 13:11:29 -07:00
👉 We've created your design skeleton at ${chalk.green('designs/' + name)}
2025-04-21 18:20:52 +02:00
👉 We've added ${chalk.green(name)} to the FreeSewing collection
2025-04-21 18:20:52 +02:00
🚧 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'
)}
2025-04-21 18:20:52 +02:00
🚀 You can now start the studio with ${chalk.blue('npm run studio')}
📖 Documentation is available at ${chalk.green('https://freesewing.dev/')}
🤓 Happy hacking
`)
}
}
2025-04-21 18:20:52 +02:00
async function addPlugin({ name }) {
2023-04-18 13:13:08 +00:00
if (name) {
2025-04-21 18:20:52 +02:00
const valid = validatePluginName(name)
if (valid !== true) {
console.log(valid)
process.exit()
}
2023-04-18 13:13:08 +00:00
createPlugin(name)
execSync('npm run reconfigure')
2025-04-21 18:20:52 +02:00
console.log(`
2023-04-18 13:13:08 +00:00
👉 We've created your plugin skeleton at ${chalk.green('plugins/plugin-' + name)}
2025-04-21 18:20:52 +02:00
🚧 We used a placeholder description; Update it in ${chalk.green(
'plugins/plugin-' + name + '/about.json'
)}
2025-04-21 18:20:52 +02:00
👷 To make your plugin do something awesome, edit ${chalk.green(
'plugins/plugin-' + name + '/src/index.mjs'
)}
2023-04-18 13:13:08 +00:00
📖 Documentation is available at ${chalk.green('https://freesewing.dev/')}
🤓 Happy hacking
`)
}
}
2023-04-18 13:20:40 +00:00
function validateDesignName(name) {
2025-04-21 18:20:52 +02:00
if (Object.keys(designs).includes(name.toLowerCase()))
2022-10-02 13:11:29 -07:00
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
else
return ' 🙈 Please use only lowercase letters, digits, or underscores. Names must start with a lowercase letter. 🤷'
}
2023-04-18 13:20:40 +00:00
function validatePluginName(name) {
2023-04-18 13:13:08 +00:00
const pluginName = 'plugin-' + name
2025-04-21 18:20:52 +02:00
if (Object.keys(plugins).includes(pluginName.toLowerCase()))
2023-04-18 13:13:08 +00:00
return `Sorry but ${pluginName} is already taken so you'll need to pick something else`
2025-04-27 14:50:28 +02:00
if (/^([a-z-]+)$/.test(name)) return true
else return ' 🙈 Please use only [a-z] or dash, no spaces, no capitals, no nothing 🤷'
2023-04-18 13:13:08 +00:00
}
2025-04-26 15:29:25 +02:00
async function createDesign(name, template) {
const src = ['packages', 'studio', 'template', 'designs', `.${template}`]
const target = ['designs', name]
const Name = name.charAt(0).toUpperCase() + name.slice(1)
// Copy template folder
await copyFolderRecursively(src, target)
// Template various mustache files
const files = (await globDir(target, '**/*.mustache'))
.map((file) => file.split(`designs${path.sep}${name}${path.sep}`).pop().split(path.sep))
.map((found) => ({
from: [...target, ...found],
to: [...target, ...found.slice(0, -1), found.slice(-1).pop().split('.mustache')[0]],
}))
for (const file of files) {
await templateOut(file.from, file.to, { name, Name })
await rm(file.from)
2022-10-02 13:11:29 -07:00
}
2025-04-26 15:29:25 +02:00
// Create about.json
await writeJsonFile([...target, 'about.json'], {
id: name,
code: 'Your name here',
design: 'Your name here',
description: 'A FreeSewing pattern that needs a description',
name: `${Name} Something`,
difficulty: 2,
tags: [],
techniques: [],
version,
pkg: `@freesewing/${name}`,
2022-10-02 13:11:29 -07:00
})
}
2023-04-18 13:13:08 +00:00
function createPlugin(name) {
const pluginName = 'plugin-' + name
const template = ['config', 'templates', 'plugin']
2023-04-18 13:29:54 +00:00
const description = 'A FreeSewing plugin that needs a description'
2023-04-18 13:13:08 +00:00
const plugin = ['plugins', pluginName]
const capitalized_name = name.charAt(0).toUpperCase() + name.slice(1)
// Create folders
mkdir([...plugin, 'src'])
mkdir([...plugin, 'tests'])
// Create package.json
templateOut([...template, 'package.json.mustache'], [...plugin, 'package.json'], {
pluginName,
description,
})
2025-04-21 18:20:52 +02:00
//Create about.json
templateOut([...template, 'about.json.mustache'], [...plugin, 'about.json'], { name })
2023-04-18 13:13:08 +00:00
// Create index.mjs
templateOut([...template, 'src', 'index.mjs.mustache'], [...plugin, 'src', 'index.mjs'], {
name,
capitalized_name,
})
}
function orderDesigns(designs) {
// Ensure designs are listed alphabetically
const newDesigns = {}
for (const type in designs) {
newDesigns[type] = {}
for (const design of Object.keys(designs[type]).sort()) {
newDesigns[type][design] = designs[type][design]
}
}
return newDesigns
}
2023-04-18 13:13:08 +00:00
function orderPlugins(plugins) {
// Ensure plugins are listed alphabetically
const newPlugins = {}
for (const plugin of Object.keys(plugins).sort()) {
newPlugins[plugin] = plugins[plugin]
}
return newPlugins
}
2025-04-21 18:20:52 +02:00
// 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)