Skip to content

Commit

Permalink
feat(migrator): wip migrate (currently only in create)
Browse files Browse the repository at this point in the history
  • Loading branch information
Guillaume Chau authored and Akryum committed Dec 21, 2018
1 parent dc21067 commit c7dbcb2
Show file tree
Hide file tree
Showing 47 changed files with 1,040 additions and 331 deletions.
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -8,13 +8,14 @@
"clean": "rimraf packages/test/*",
"sync": "node scripts/syncDeps.js",
"release": "lerna publish",
"test": "yarn run test:types",
"test": "yarn run test:types && lerna run test",
"test:types": "tsc"
},
"devDependencies": {
"@types/ejs": "^2.6.0",
"@types/events": "^1.2.0",
"@types/fs-extra": "^5.0.4",
"@types/globby": "^8.0.0",
"@types/inquirer": "^0.0.43",
"@types/node": "^10.12.5",
"@types/ora": "^1.3.4",
Expand Down
@@ -1,7 +1,7 @@
{
"name": "@nodepack/generator",
"name": "@nodepack/app-migrator",
"version": "0.0.1",
"description": "Generator API for nodepack",
"description": "Migration system for nodepack",
"author": "Guillaume Chau <guillaume.b.chau@gmail.com>",
"license": "MIT",
"repository": {
Expand All @@ -16,18 +16,22 @@
"access": "public"
},
"main": "src/index.js",
"typings": "types/index.d.ts",
"scripts": {
"test": "yarn test:lint",
"test:lint": "eslint src"
},
"dependencies": {
"@nodepack/config-transformer": "^0.0.1",
"@nodepack/module": "^0.0.1",
"@nodepack/service": "^0.0.1",
"@nodepack/utils": "^0.0.1",
"@types/inquirer": "^0.0.43",
"deepmerge": "^3.0.0",
"ejs": "^2.6.1",
"fs-extra": "^7.0.1",
"globby": "^8.0.1",
"inquirer": "^6.2.1",
"isbinaryfile": "^3.0.3",
"lodash.clonedeep": "^4.5.0",
"semver": "^5.6.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/@nodepack/app-migrator/src/index.js
@@ -0,0 +1,4 @@
exports.Migrator = require('./lib/Migrator')
exports.MigratorPlugin = require('./lib/MigratorPlugin')
exports.MigrationOperationFile = require('./lib/MigrationOperationFile')
exports.writeFileTree = require('./util/writeFileTree')
35 changes: 35 additions & 0 deletions packages/@nodepack/app-migrator/src/lib/MigrationAPI.js
@@ -0,0 +1,35 @@
/** @typedef {import('../../types/MigrationOptions').MigrationOptions} MigrationOptions */
/** @typedef {import('./Migrator')} Migrator */
/** @typedef {import('./MigratorPlugin')} MigratorPlugin */

module.exports = class MigrationAPI {
/**
* @param {MigratorPlugin} plugin
* @param {Migrator} migrator
*/
constructor (plugin, migrator) {
this.plugin = plugin
this.migrator = migrator
}

/**
* Register a migration that may be run during plugin install or dev build.
*
* @param {MigrationOptions} options
*/
register (options) {
this.migrator.migrations.push({
plugin: this.plugin,
options,
})
}

/**
* Called once after all migration operations are completed.
*
* @param {function} cb
*/
onComplete (cb) {
this.migrator.completeCbs.push(cb)
}
}
@@ -1,36 +1,28 @@
/** @typedef {import('@nodepack/service').ProjectOptions} ProjectOptions */
/** @typedef {import('./GeneratorPlugin')} GeneratorPlugin */
/** @typedef {import('./Migrator')} Migrator */
/** @typedef {import('./Migrator').Migration} Migration */

/**
* @typedef GeneratorOptions
* @prop {any} [pkg]
* @prop {GeneratorPlugin []} [plugins]
* @prop {ProjectOptions} [projectOptions]
* @typedef MigratorOperationOptions
* @prop {function []} [completeCbs]
* @prop {boolean} [invoking]
* @prop {any} [options] Options from migration prompts
* @prop {any} [rootOptions] Options from all migration prompts
*/

/** @typedef {Object.<string, GeneratorFile>} FileTree */
/**
* @typedef ExitLog
* @prop {string} id
* @prop {string} msg
* @prop {'log' | 'info' | 'done' | 'warn' | 'error'} type
*/
/** @typedef {(files: FileTree, render: function) => Promise | void} FileMiddleware */
/** @typedef {(files: FileTree) => Promise | void} FilePostProcessor */

// API
const MigrationOperationAPI = require('./MigrationOperationAPI')
// Utils
const ejs = require('ejs')
const GeneratorPluginAPI = require('./GeneratorPluginAPI')
const cloneDeep = require('lodash.clonedeep')
const { ConfigTransform } = require('@nodepack/config-transformer')
const {
ensureEOL,
sortObject,
matchesPluginId,
toShortPluginId,
} = require('@nodepack/utils')
const GeneratorFile = require('./GeneratorFile')
const { ensureEOL } = require('@nodepack/utils')
const GeneratorFile = require('./MigrationOperationFile')
const writeFileTree = require('../util/writeFileTree')
const normalizeFilePaths = require('../util/normalizeFilePaths')
const { sortPkg, readPkg } = require('../util/pkg')
const { readFiles, normalizeFilePaths } = require('../util/files')

const defaultConfigTransforms = {
babel: new ConfigTransform({
Expand Down Expand Up @@ -60,35 +52,23 @@ const reservedConfigTransforms = {
}),
}

const logger = require('@nodepack/utils/src/logger')
const logTypes = {
log: logger.log,
info: logger.info,
done: logger.done,
warn: logger.warn,
error: logger.error,
}

module.exports = class Generator {
module.exports = class MigrationOperation {
/**
* @param {string} cwd
* @param {GeneratorOptions} options
* @param {Migrator} migrator
* @param {Migration} migration
* @param {MigratorOperationOptions} options
*/
constructor (cwd, {
pkg = {},
plugins = [],
projectOptions = {},
constructor (migrator, migration, {
completeCbs = [],
invoking = false,
}) {
this.cwd = cwd
this.originalPkg = pkg
this.pkg = cloneDeep(pkg)
this.plugins = plugins
this.projectOptions = projectOptions
this.rootOptions = {}
options = {},
rootOptions = {},
} = {}) {
this.migrator = migrator
this.migration = migration
this.completeCbs = completeCbs
this.invoking = invoking
this.options = options
this.rootOptions = rootOptions

// Config file transforms
this.configTransforms = {}
this.defaultConfigTransforms = defaultConfigTransforms
Expand All @@ -102,50 +82,77 @@ module.exports = class Generator {
this.fileMiddlewares = []
/** @type {FilePostProcessor []} */
this.postProcessFilesCbs = []
/**
* exit messages displayed at the end
* @type {ExitLog []}
*/
this.exitLogs = []
}

async generate ({
extractConfigFiles = false,
checkExisting = false,
} = {}) {
if (this.invoking) {
// Read existing files
await this.readFiles()
}
get cwd () {
return this.migrator.cwd
}

/**
* @param {object} param
* @param {boolean} param.extractConfigFiles
*/
async migrate ({
extractConfigFiles,
}) {
// Read existing files
await this.readFiles()

const previousFileNames = Object.keys(this.files)

await this.applyPlugins()
await this.migration.options.migrate(new MigrationOperationAPI(this), this.options, this.rootOptions)

// extract configs from package.json into dedicated files.
this.extractConfigFiles(extractConfigFiles, checkExisting)
this.extractConfigFiles(extractConfigFiles, true)

// wait for file resolve
await this.resolveFiles()

// set package.json
this.sortPkg()
this.pkg = sortPkg(this.pkg)
this.writeFile('package.json', JSON.stringify(this.pkg, null, 2))

// write/update file tree to disk
await writeFileTree(this.cwd, this.files, previousFileNames)
}

async rollback () {
// TODO
}

/**
* Read the project file tree
* Read the project file tree.
*
* @private
*/
async readFiles () {
const readFiles = require('../util/readFiles')
this.originalPkg = await readPkg(this.cwd)
this.pkg = await readPkg(this.cwd)
this.files = await readFiles(this.cwd)
}

/**
* Write a file content into virtual filesystem.
*
* @param {string} filename
* @param {string | Buffer} source
*/
writeFile (filename, source, files = this.files) {
if (typeof source === 'string') {
source = ensureEOL(source)
}
const file = this.files[filename]
if (file) {
file.source = source
} else {
this.files[filename] = new GeneratorFile(filename, source, true)
}
}

/**
* Extract config files into separate files.
*
* @private
* @param {boolean} extractAll
* @param {boolean} checkExisting
*/
Expand Down Expand Up @@ -192,61 +199,10 @@ module.exports = class Generator {
}

/**
* Write a file content into virtual filesystem
* @param {string} filename
* @param {string | Buffer} source
*/
writeFile (filename, source, files = this.files) {
if (typeof source === 'string') {
source = ensureEOL(source)
}
const file = this.files[filename]
if (file) {
file.source = source
} else {
this.files[filename] = new GeneratorFile(filename, source, true)
}
}

/**
* Apply middlewares, normalization and file post processing.
*
* @private
*/
async applyPlugins () {
// apply plugins
for (const { id, apply } of this.plugins) {
await apply(new GeneratorPluginAPI(id, this), this.projectOptions || {})
}
}

sortPkg () {
// ensure package.json keys has readable order
this.pkg.dependencies = sortObject(this.pkg.dependencies)
this.pkg.devDependencies = sortObject(this.pkg.devDependencies)
this.pkg.scripts = sortObject(this.pkg.scripts, [
'serve',
'build',
'test',
'e2e',
'lint',
'deploy',
])
this.pkg = sortObject(this.pkg, [
'name',
'version',
'private',
'description',
'author',
'scripts',
'dependencies',
'devDependencies',
'nodepack',
'babel',
'eslintConfig',
'prettier',
'jest',
])
}

async resolveFiles () {
const files = this.files
for (const middleware of this.fileMiddlewares) {
Expand All @@ -261,40 +217,4 @@ module.exports = class Generator {
await postProcess(files)
}
}

/**
* Check if the project has a plugin installed
* @param {string} id Plugin id
*/
hasPlugin (id) {
if (id === 'router') id = 'vue-router'
if (['vue-router', 'vuex'].includes(id)) {
const pkg = this.pkg
return ((pkg.dependencies && pkg.dependencies[id]) || (pkg.devDependencies && pkg.devDependencies[id]))
}
return [
...this.plugins.map(p => p.id),
...Object.keys(this.pkg.devDependencies || {}),
...Object.keys(this.pkg.dependencies || {}),
].some(name => matchesPluginId(name, id))
}

/**
* Output the final logs pushed by the plugins
*/
printExitLogs () {
if (this.exitLogs.length) {
this.exitLogs.forEach(({ id, msg, type }) => {
const shortId = toShortPluginId(id)
const logFn = logTypes[type]
if (!logFn) {
logger.error(`Invalid api.exitLog type '${type}'.`, shortId)
} else {
const tag = msg ? shortId : null
logFn(msg, tag)
}
})
logger.log()
}
}
}

0 comments on commit c7dbcb2

Please sign in to comment.