-
Notifications
You must be signed in to change notification settings - Fork 85
feat: tutorialkit eject
command
#81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f68571f
4513bfc
f1b66af
3228705
98740da
bc536b9
e303332
ce68177
2047b0d
72bffb2
e374027
17d194c
fe1211d
0a8b9dd
b3a8135
499dd36
f00eeee
ddb7ab6
fc2bde2
e8a1f7a
9fd7ee0
4013b62
a1f1aff
f5ca361
75925a8
3cafab6
1033feb
be68c98
c03d28e
0e2b6d8
a52d0bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
import * as prompts from '@clack/prompts'; | ||
import chalk from 'chalk'; | ||
import detectIndent from 'detect-indent'; | ||
import { execa } from 'execa'; | ||
import fs from 'node:fs'; | ||
import path from 'node:path'; | ||
import whichpm from 'which-pm'; | ||
import type { Arguments } from 'yargs-parser'; | ||
import { pkg } from '../../pkg.js'; | ||
import { generateAstroConfig, parseAstroConfig, replaceArgs } from '../../utils/astro-config.js'; | ||
import { errorLabel, primaryLabel, printHelp } from '../../utils/messages.js'; | ||
import { updateWorkspaceVersions } from '../../utils/workspace-version.js'; | ||
import { DEFAULT_VALUES, type EjectOptions } from './options.js'; | ||
|
||
interface PackageJson { | ||
dependencies: Record<string, string>; | ||
devDependencies: Record<string, string>; | ||
} | ||
|
||
const TUTORIALKIT_VERSION = pkg.version; | ||
const REQUIRED_DEPENDENCIES = ['@tutorialkit/runtime', '@webcontainer/api', 'nanostores', '@nanostores/react']; | ||
|
||
export function ejectRoutes(flags: Arguments) { | ||
if (flags._[1] === 'help' || flags.help || flags.h) { | ||
printHelp({ | ||
commandName: `${pkg.name} eject`, | ||
usage: '[folder] [...options]', | ||
tables: { | ||
Options: [ | ||
[ | ||
'--force', | ||
`Overwrite existing files in the target directory without prompting (default ${chalk.yellow(DEFAULT_VALUES.force)})`, | ||
], | ||
['--defaults', 'Skip all the prompts and eject the routes using the defaults'], | ||
], | ||
}, | ||
}); | ||
|
||
return 0; | ||
} | ||
|
||
try { | ||
return _eject(flags); | ||
} catch (error) { | ||
console.error(`${errorLabel()} Command failed`); | ||
|
||
if (error.stack) { | ||
console.error(`\n${error.stack}`); | ||
} | ||
|
||
process.exit(1); | ||
} | ||
} | ||
|
||
async function _eject(flags: EjectOptions) { | ||
let folderPath = flags._[1] !== undefined ? String(flags._[1]) : undefined; | ||
|
||
if (folderPath === undefined) { | ||
folderPath = process.cwd(); | ||
} else { | ||
folderPath = path.resolve(process.cwd(), folderPath); | ||
} | ||
|
||
/** | ||
* First we make sure that the destination has the correct files | ||
* and that there won't be any files overwritten in the process. | ||
* | ||
* If there are any and `force` was not specified we abort. | ||
*/ | ||
const { astroConfigPath, srcPath, pkgJsonPath, astroIntegrationPath, srcDestPath } = validateDestination( | ||
folderPath, | ||
flags.force, | ||
); | ||
|
||
/** | ||
* We proceed with the astro configuration. | ||
* | ||
* There we must disable the default routes so that the | ||
* new routes that we're copying will be automatically picked up. | ||
*/ | ||
const astroConfig = await parseAstroConfig(astroConfigPath); | ||
|
||
replaceArgs({ defaultRoutes: false }, astroConfig); | ||
|
||
fs.writeFileSync(astroConfigPath, generateAstroConfig(astroConfig)); | ||
|
||
// we copy all assets from the `default` folder into the `src` folder | ||
fs.cpSync(srcPath, srcDestPath, { recursive: true }); | ||
|
||
/** | ||
* Last, we ensure that the `package.json` contains the extra dependencies. | ||
* If any are missing we suggest to install the new dependencies. | ||
*/ | ||
const pkgJsonContent = fs.readFileSync(pkgJsonPath, 'utf-8'); | ||
const indent = detectIndent(pkgJsonContent).indent || ' '; | ||
const pkgJson: PackageJson = JSON.parse(pkgJsonContent); | ||
|
||
const astroIntegrationPkgJson: PackageJson = JSON.parse( | ||
fs.readFileSync(path.join(astroIntegrationPath, 'package.json'), 'utf-8'), | ||
); | ||
|
||
const newDependencies = []; | ||
|
||
for (const dep of REQUIRED_DEPENDENCIES) { | ||
if (!(dep in pkgJson.dependencies) && !(dep in pkgJson.devDependencies)) { | ||
pkgJson.dependencies[dep] = astroIntegrationPkgJson.dependencies[dep]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe not super important for now, but we might want to check the version of the dependency and warn the user that they might need to upgrade/downgrade or something. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We do! The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm so technically this can result in false positives if say I have
Given that there's no new patch version and There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If they have an incompatible version of If it's compatible, then this code uses their version, so I think this code is good? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea I think it's fine cause we only show the warning if that dependency is missing entirely in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah good point! Oh yeah that would be really nice actually. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One possible way would be to leverage |
||
|
||
newDependencies.push(dep); | ||
} | ||
} | ||
|
||
updateWorkspaceVersions(pkgJson.dependencies, TUTORIALKIT_VERSION, (dependency) => | ||
SamVerschueren marked this conversation as resolved.
Show resolved
Hide resolved
|
||
REQUIRED_DEPENDENCIES.includes(dependency), | ||
); | ||
|
||
if (newDependencies.length > 0) { | ||
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, undefined, indent), { encoding: 'utf-8' }); | ||
|
||
console.log( | ||
primaryLabel('INFO'), | ||
`New dependencies added: ${newDependencies.join(', ')}. Install the new dependencies before proceeding.`, | ||
); | ||
|
||
if (!flags.defaults) { | ||
const packageManager = (await whichpm(path.dirname(pkgJsonPath))).name; | ||
|
||
const answer = await prompts.confirm({ | ||
message: `Do you want to install those dependencies now using ${chalk.blue(packageManager)}?`, | ||
}); | ||
|
||
if (answer === true) { | ||
await execa(packageManager, ['install'], { cwd: folderPath, stdio: 'inherit' }); | ||
} | ||
} | ||
} | ||
} | ||
|
||
function validateDestination(folder: string, force: boolean) { | ||
assertExists(folder); | ||
|
||
const pkgJsonPath = assertExists(path.join(folder, 'package.json')); | ||
const astroConfigPath = assertExists(path.join(folder, 'astro.config.ts')); | ||
const srcDestPath = assertExists(path.join(folder, 'src')); | ||
|
||
const astroIntegrationPath = assertExists(path.resolve(folder, 'node_modules', '@tutorialkit', 'astro')); | ||
|
||
const srcPath = path.join(astroIntegrationPath, 'dist', 'default'); | ||
|
||
// check that there are no collision | ||
if (!force) { | ||
walk(srcPath, (relativePath) => { | ||
const destination = path.join(srcDestPath, relativePath); | ||
|
||
if (fs.existsSync(destination)) { | ||
throw new Error( | ||
`Eject aborted because '${destination}' would be overwritten by this command. Use ${chalk.yellow('--force')} to ignore this error.`, | ||
); | ||
} | ||
}); | ||
} | ||
|
||
return { | ||
astroConfigPath, | ||
astroIntegrationPath, | ||
pkgJsonPath, | ||
srcPath, | ||
srcDestPath, | ||
}; | ||
} | ||
|
||
function assertExists(filePath: string) { | ||
if (!fs.existsSync(filePath)) { | ||
throw new Error(`${filePath} does not exists!`); | ||
} | ||
|
||
return filePath; | ||
} | ||
|
||
function walk(root: string, visit: (relativeFilePath: string) => void) { | ||
function traverse(folder: string, pathPrefix: string) { | ||
for (const filename of fs.readdirSync(folder)) { | ||
const filePath = path.join(folder, filename); | ||
const stat = fs.statSync(filePath); | ||
|
||
const relativeFilePath = path.join(pathPrefix, filename); | ||
|
||
if (stat.isDirectory()) { | ||
traverse(filePath, relativeFilePath); | ||
} else { | ||
visit(relativeFilePath); | ||
} | ||
} | ||
} | ||
|
||
traverse(root, ''); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
export interface EjectOptions { | ||
_: Array<string | number>; | ||
force?: boolean; | ||
defaults?: boolean; | ||
} | ||
|
||
export const DEFAULT_VALUES = { | ||
force: false, | ||
defaults: false, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,12 +3,13 @@ | |
import chalk from 'chalk'; | ||
import yargs from 'yargs-parser'; | ||
import { createTutorial } from './commands/create/index.js'; | ||
import { ejectRoutes } from './commands/eject/index.js'; | ||
import { pkg } from './pkg.js'; | ||
import { errorLabel, primaryLabel, printHelp } from './utils/messages.js'; | ||
|
||
type CLICommand = 'version' | 'help' | 'create'; | ||
type CLICommand = 'version' | 'help' | 'create' | 'eject'; | ||
|
||
const supportedCommands = new Set(['version', 'help', 'create']); | ||
const supportedCommands = new Set<string>(['version', 'help', 'create', 'eject'] satisfies CLICommand[]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't we just type this as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can't because then we get a type error below when trying to do The reason I made that change was to make sure the values are checked against the |
||
|
||
cli(); | ||
|
||
|
@@ -53,6 +54,9 @@ async function runCommand(cmd: CLICommand, flags: yargs.Arguments): Promise<numb | |
case 'create': { | ||
return createTutorial(flags); | ||
} | ||
case 'eject': { | ||
return ejectRoutes(flags); | ||
} | ||
default: { | ||
console.error(`${errorLabel()} Unknown command ${chalk.red(cmd)}`); | ||
return 1; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
export function updateWorkspaceVersions( | ||
dependencies: Record<string, string>, | ||
version: string, | ||
filterDependency: (dependency: string) => boolean = allowAll, | ||
) { | ||
for (const dependency in dependencies) { | ||
const depVersion = dependencies[dependency]; | ||
|
||
if (depVersion === 'workspace:*' && filterDependency(dependency)) { | ||
if (process.env.TK_DIRECTORY) { | ||
const name = dependency.split('/')[1]; | ||
|
||
dependencies[dependency] = `file:${process.env.TK_DIRECTORY}/packages/${name.replace('-', '/')}`; | ||
} else { | ||
dependencies[dependency] = version; | ||
} | ||
} | ||
} | ||
} | ||
|
||
function allowAll() { | ||
return true; | ||
} |
Uh oh!
There was an error while loading. Please reload this page.