1
0
mirror of synced 2025-12-19 18:11:23 -05:00

Add base infrastructure for blitz install (#434)

(meta)
This commit is contained in:
Adam Markon
2020-05-07 09:18:59 -04:00
committed by GitHub
parent 603e5cbaf2
commit 75a7feabf9
29 changed files with 770 additions and 20 deletions

1
.github/CODEOWNERS vendored
View File

@@ -6,3 +6,4 @@
packages/server/**/* @ryardley
packages/cli/**/* @aem
packages/generator/**/* @aem
packages/installer/**/* @aem

View File

@@ -43,6 +43,7 @@
"@blitzjs/cli": "0.9.0",
"@blitzjs/core": "0.9.0",
"@blitzjs/generator": "0.9.0",
"@blitzjs/installer": "0.9.0",
"@blitzjs/server": "0.9.0",
"os-name": "^3.1.0",
"pkg-dir": "^4.2.0",

View File

@@ -6,7 +6,7 @@
"scripts": {
"b": "./bin/run",
"clean": "rimraf lib",
"predev": "wait-on ../server/dist/packages/server/src/index.d.ts && wait-on ../generator/dist/packages/generator/src/index.d.ts",
"predev": "wait-on ../installer/dist/packages/installer/src/index.d.ts && wait-on ../server/dist/packages/server/src/index.d.ts && wait-on ../generator/dist/packages/generator/src/index.d.ts",
"dev": "rimraf lib && tsc --watch --preserveWatchOutput",
"build": "rimraf lib && tsc",
"lint": "tsdx lint",
@@ -49,6 +49,7 @@
},
"devDependencies": {
"@blitzjs/generator": "0.9.0",
"@blitzjs/installer": "0.9.0",
"@blitzjs/server": "0.9.0",
"@oclif/dev-cli": "^1.22.2",
"@oclif/test": "^1.2.5",

View File

@@ -0,0 +1,42 @@
import {Command} from '../command'
import * as path from 'path'
import {Installer} from '@blitzjs/installer'
// eslint-disable-next-line import/no-default-export
export default class Install extends Command {
static description = 'Install a third-party package into your Blitz app'
static aliases = ['i']
static strict = false
static hidden = true
static args = [
{
name: 'installer',
required: true,
description: 'Name of a Blitz installer from @blitzjs/installers, or a file path to a local installer',
},
{
name: 'installer-flags',
description:
'A list of flags to pass to the installer. Blitz will only parse these in the form key=value',
},
]
async run() {
const {args} = this.parse(Install)
const isNavtiveInstaller = /^([\w]*)$/.test(args.installer)
if (isNavtiveInstaller) {
} else {
const installerPath = path.resolve(args.installer)
const installer = require(installerPath).default as Installer<any>
const installerArgs = this.argv.reduce(
(acc, arg) => ({
...acc,
[arg.split('=')[0]]: JSON.parse(arg.split('=')[1] || String(true)), // if no value is provided, assume it's a boolean flag
}),
{},
)
await installer.run(installerArgs)
}
}
}

View File

@@ -45,7 +45,7 @@ export class ConflictChecker extends Transform {
if (status !== 'skip') {
this.handlePush(file, status)
} else {
this.fileStatusString(file, status)
this.fileStatusString(file, status, this.options?.dryRun)
}
cb()
@@ -71,7 +71,7 @@ export class ConflictChecker extends Transform {
handlePush(file: File, status: PromptActions): void {
if (!this.options?.dryRun) this.push(file)
this.emit('fileStatus', this.fileStatusString(file, status))
this.emit('fileStatus', this.fileStatusString(file, status, this.options?.dryRun))
}
private async checkDiff(file: File): Promise<PromptActions> {
@@ -120,14 +120,14 @@ export class ConflictChecker extends Transform {
console.log('\n')
}
private fileStatusString(file: File, status: PromptActions) {
private fileStatusString(file: File, status: PromptActions, dryRun: boolean = false) {
let statusLog = null
switch (status) {
case 'create':
statusLog = chalk.green('CREATE ')
statusLog = chalk.green(`${dryRun ? 'Would create' : 'CREATE'} `)
break
case 'overwrite':
statusLog = chalk.cyan('OVERWRITE')
statusLog = chalk.cyan(`${dryRun ? 'Would overwrite' : 'OVERWRITE'} `)
break
case 'skip':
statusLog = chalk.blue('SKIP ')

View File

@@ -3,3 +3,5 @@ export * from './generators/model-generator'
export * from './generators/mutation-generator'
export * from './generators/page-generator'
export * from './generators/query-generator'
export * from './generator'
export * from './conflict-checker'

15
packages/installer/.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
*.log
.DS_Store
node_modules
.rts2_cache_cjs
.rts2_cache_esm
.rts2_cache_umd
.rts2_cache_system
dist
tmp
.blitz
# good directory to use for testing app generation
_app
!templates/**/.env

View File

@@ -0,0 +1,5 @@
# `installer`
The installer package houses all of the types, classes, and utilities for building a Blitz Installer. A Blitz installer is effectively just a list of steps, represented as an array of objects that conform to one of the types in the `Executor` union type (`NewFileExecutor | AddDependencyExecutor } FileTransformExecutor`). These executors are processed by the framework, executed interactively by the user, and ultimately run to install new packages to an existing Blitz app.
You can find the implementation of all Executors in the `executors/` directory, stock transforms that we'll be supplying to authors in `transforms/`, and various utilities in `utils/`, including a `paths` utility that the user can access for common paths to modify such as `_document.tsx`.

View File

@@ -0,0 +1,25 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['ts', 'js', 'json'],
coverageReporters: ['json', 'lcov', 'text', 'clover'],
// collectCoverage: !!`Boolean(process.env.CI)`,
collectCoverageFrom: ['src/**/*.ts'],
modulePathIgnorePatterns: ['<rootDie>/tmp', '<rootDir>/dist'],
// TODO enable threshold
// coverageThreshold: {
// global: {
// branches: 100,
// functions: 100,
// lines: 100,
// statements: 100,
// },
// },
globals: {
'ts-jest': {
tsConfig: 'test/tsconfig.json',
isolatedModules: true,
},
},
}

View File

@@ -0,0 +1,56 @@
{
"name": "@blitzjs/installer",
"version": "0.9.0",
"description": "Package installation for the Blitz CLI",
"homepage": "https://github.com/blitz-js/blitz#readme",
"license": "MIT",
"scripts": {
"predev": "wait-on ../generator/dist/packages/generator/src/index.d.ts && wait-on ../server/dist/packages/server/src/index.d.ts",
"dev": "tsdx watch --verbose",
"build": "tsdx build",
"test": "tsdx test",
"test:watch": "tsdx test --watch",
"lint": "tsdx lint"
},
"author": {
"name": "Brandon Bayer",
"email": "b@bayer.ws",
"url": "https://twitter.com/flybayer"
},
"main": "dist/index.js",
"module": "dist/installer.esm.js",
"types": "dist/packages/installer/src/index.d.ts",
"files": [
"dist"
],
"husky": {
"hooks": {
"pre-commit": "tsdx lint"
}
},
"keywords": [
"blitz",
"installer"
],
"repository": {
"type": "git",
"url": "git+https://github.com/blitz-js/blitz.git"
},
"dependencies": {
"@babel/core": "^7.9.0",
"@babel/plugin-transform-typescript": "^7.9.4",
"ast-types": "^0.13.3",
"cross-spawn": "^7.0.2",
"diff": "^4.0.2",
"enquirer": "^2.3.5",
"fs-extra": "^9.0.0",
"globby": "11.0.0",
"recast": "^0.19.1",
"ts-node": "^8.9.1"
},
"devDependencies": {
"@blitzjs/generator": "0.9.0",
"@blitzjs/server": "0.9.0",
"@types/node": "^13.13.4"
}
}

View File

@@ -0,0 +1,47 @@
import {BaseExecutor, executorArgument, getExecutorArgument} from './executor'
import * as fs from 'fs-extra'
import * as path from 'path'
import spawn from 'cross-spawn'
import {log} from '@blitzjs/server/src/log'
interface NpmPackage {
name: string
// defaults to latest published
version?: string
// defaults to false
isDevDep?: boolean
}
export interface AddDependencyExecutor extends BaseExecutor {
packages: executorArgument<NpmPackage[]>
}
export function isAddDependencyExecutor(executor: BaseExecutor): executor is AddDependencyExecutor {
return (executor as AddDependencyExecutor).packages !== undefined
}
async function getPackageManager(): Promise<'yarn' | 'npm'> {
if (fs.existsSync(path.resolve('package-lock.json'))) {
return 'npm'
}
return 'yarn'
}
export async function addDependencyExecutor(executor: AddDependencyExecutor, cliArgs: any): Promise<void> {
const packageManager = await getPackageManager()
const packagesToInstall = getExecutorArgument(executor.packages, cliArgs)
for (const pkg of packagesToInstall) {
const args: string[] = ['add']
// if devDep flag isn't specified we install as a regular dependency, so
// we need to explicitly check for `true`
if (pkg.isDevDep === true) {
args.push(packageManager === 'yarn' ? '-D' : '--save-dev')
}
pkg.version ? args.push(`${pkg.name}@${pkg.version}`) : args.push(pkg.name)
log.meta(`Installing ${pkg.name} ${pkg.isDevDep !== false ? 'as a dev dependency' : ''}`)
spawn.sync(packageManager, args, {
stdio: ['inherit', 'pipe', 'pipe'],
})
}
log.progress(`${packagesToInstall.length} packages installed successfully`)
}

View File

@@ -0,0 +1,34 @@
import {log} from '@blitzjs/server/src/log'
export interface BaseExecutor {
stepId: string | number
stepName: string
// a bit to display to the user to give context to the change
explanation: string
}
type dynamicExecutorArgument<T> = (cliArgs: any) => T
function isDynamicExecutorArgument<T>(input: executorArgument<T>): input is dynamicExecutorArgument<T> {
return typeof (input as dynamicExecutorArgument<T>) === 'function'
}
export type executorArgument<T> = T | dynamicExecutorArgument<T>
export function logExecutorFrontmatter(executor: BaseExecutor) {
console.log()
const lineLength = executor.stepName.length + 6
const verticalBorder = `+${new Array(lineLength).fill('').join('')}+`
log.branded(verticalBorder)
log.branded(`${executor.stepName}`)
log.branded(verticalBorder)
log.info(executor.explanation)
console.log()
}
export function getExecutorArgument<T>(input: executorArgument<T>, cliArgs: any): T {
if (isDynamicExecutorArgument(input)) {
return input(cliArgs)
}
return input
}

View File

@@ -0,0 +1,36 @@
import {prompt as enquirer} from 'enquirer'
import globby from 'globby'
enum SearchType {
file,
directory,
}
interface FilePromptOptions {
globFilter?: string
getChoices?(context: any): string[]
searchType?: SearchType
context: any
}
async function getMatchingFiles(filter: string = ''): Promise<string[]> {
return globby(filter, {expandDirectories: true})
}
export async function filePrompt(options: FilePromptOptions): Promise<string> {
const choices = options.getChoices
? options.getChoices(options.context)
: await getMatchingFiles(options.globFilter)
if (choices.length === 1) {
return choices[0]
}
const results = await enquirer({
type: 'autocomplete',
name: 'file',
message: 'Select the target file',
// @ts-ignore
limit: 10,
choices,
})
return results.file
}

View File

@@ -0,0 +1,58 @@
import {BaseExecutor, executorArgument, getExecutorArgument} from './executor'
import {filePrompt} from './file-prompt'
import {transform, Transformer} from '../utils/transform'
import {log} from '@blitzjs/server/src/log'
import {waitForConfirmation} from '../utils/wait-for-confirmation'
import {createPatch} from 'diff'
import chokidar from 'chokidar'
import * as fs from 'fs-extra'
import chalk from 'chalk'
export interface FileTransformExecutor extends BaseExecutor {
selectTargetFiles?(cliArgs: any): any[]
singleFileSearch?: executorArgument<string>
transform: Transformer
}
export function isFileTransformExecutor(executor: BaseExecutor): executor is FileTransformExecutor {
return (executor as FileTransformExecutor).transform !== undefined
}
async function executeWithDiff(transformFn: Transformer, filePath: string) {
await new Promise((res, rej) => {
const watcher = chokidar.watch(filePath)
const originalFileContents = fs.readFileSync(filePath).toString('utf-8')
watcher.on('change', (path) => {
watcher.close().then(() => {
const patch = createPatch(path, originalFileContents, fs.readFileSync(path).toString('utf-8'))
patch
.split('\n')
.slice(2)
.forEach((line) => {
if (line[0] === '-') console.log(chalk.bold.red(line))
else if (line[0] === '+') console.log(chalk.bold.green(line))
else console.log(line)
})
res(path)
})
})
watcher.on('error', (error) => {
rej(error)
})
transform(transformFn, [filePath])
})
}
export async function fileTransformExecutor(executor: FileTransformExecutor, cliArgs: any): Promise<void> {
const fileToTransform: string = await filePrompt({
context: cliArgs,
globFilter: getExecutorArgument(executor.singleFileSearch, cliArgs),
getChoices: executor.selectTargetFiles,
})
try {
await executeWithDiff(executor.transform, fileToTransform)
await waitForConfirmation('The above changes were applied. Press enter to continue')
} catch (err) {
log.error(`Failed to transform ${fileToTransform}`)
}
}

View File

@@ -0,0 +1,60 @@
import {BaseExecutor, executorArgument, getExecutorArgument} from './executor'
import {Generator, GeneratorOptions} from '@blitzjs/generator'
import {log} from '@blitzjs/server/src/log'
import {waitForConfirmation} from '../utils/wait-for-confirmation'
export interface NewFileExecutor extends BaseExecutor {
targetDirectory?: executorArgument<string>
templatePath: executorArgument<string>
templateValues: executorArgument<{[key: string]: string}>
destinationPathPrompt?: executorArgument<string>
}
export function isNewFileExecutor(executor: BaseExecutor): executor is NewFileExecutor {
return (executor as NewFileExecutor).templatePath !== undefined
}
interface TempGeneratorOptions extends GeneratorOptions {
targetDirectory?: string
templateRoot: string
templateValues: any
}
class TempGenerator extends Generator<TempGeneratorOptions> {
sourceRoot: string
targetDirectory: string
templateValues: any
constructor(options: TempGeneratorOptions) {
super(options)
this.sourceRoot = options.templateRoot
this.templateValues = options.templateValues
this.targetDirectory = options.targetDirectory || '.'
}
getTemplateValues() {
return this.templateValues
}
getTargetDirectory() {
return this.targetDirectory
}
}
export async function newFileExecutor(executor: NewFileExecutor, cliArgs: any): Promise<void> {
const generatorArgs = {
destinationRoot: '.',
targetDirectory: getExecutorArgument(executor.targetDirectory, cliArgs),
templateRoot: getExecutorArgument(executor.templatePath, cliArgs),
templateValues: getExecutorArgument(executor.templateValues, cliArgs),
}
const dryRunGenerator = new TempGenerator({
...generatorArgs,
dryRun: true,
})
const commitGenerator = new TempGenerator(generatorArgs)
log.progress("First we'll do a dry-run. Here's a list of files that would be created:")
await dryRunGenerator.run()
await waitForConfirmation('To commit the changes, press enter. Press Ctrl+C to abort')
await commitGenerator.run()
}

View File

@@ -0,0 +1,8 @@
export * from './installer'
export * from './executors/executor'
export * from './executors/add-dependency-executor'
export * from './executors/file-transform-executor'
export * from './executors/new-file-executor'
export * from './utils/paths'
export * from './transforms'
export {customTsParser} from './utils/transform'

View File

@@ -0,0 +1,129 @@
/*
An installer is a package that has a single index.(ts|js) file with a single
default export. Its type is
class Installer<Options extends InstallerOptions> {
steps: InstallerStep[]
options: Options
}
The Installer class is, at its core, a flexible step execution framework for
clients. It's likely that much of this code can be reused for plugins when
the time comes.
The client exporting its own instance of Installer allows installers authored
in TypeScript to ensure thier installer conforms to the Installer's interface.
In the Blitz CLI, the `install` command will know how to fetch installers from
the Blitz-hosted installer set. Alternatively, you could supply a relative
filesystem path to an installer or an absolute URL to a GitHub repo that houses
an installer.
The `install` command will read the package, execute the script steps, guiding
the user through the installation step-by-step based on the `steps` config.
Any extra CLI args passed will be parsed into a JS object and passed directly
to each installer step and lifecycle method.
We'll begin by supporting three step types, or executors: transform files, add
files, and add dependencies. These steps are each strongly typed and have
strict validation, including requirements for explanations of the changes
provided. We'll use these fields to create the wizard for the end user.
*/
import {
AddDependencyExecutor,
isAddDependencyExecutor,
addDependencyExecutor,
} from './executors/add-dependency-executor'
import {NewFileExecutor, isNewFileExecutor, newFileExecutor} from './executors/new-file-executor'
import {
FileTransformExecutor,
isFileTransformExecutor,
fileTransformExecutor,
} from './executors/file-transform-executor'
import {log} from '@blitzjs/server/src/log'
import {logExecutorFrontmatter} from './executors/executor'
import {waitForConfirmation} from './utils/wait-for-confirmation'
type Executor = FileTransformExecutor | AddDependencyExecutor | NewFileExecutor
interface InstallerOptions {
packageName: string
packageDescription: string
packageOwner: string
packageRepoLink: string
validateArgs?(args: {}): Promise<void>
preInstall?(): Promise<void>
beforeEach?(stepId: string | number): Promise<void>
afterEach?(stepId: string | number): Promise<void>
postInstall?(): Promise<void>
}
export class Installer<Options extends InstallerOptions> {
private readonly steps: Executor[]
private readonly options: Options
constructor(options: Options, steps: Executor[]) {
this.options = options
this.steps = steps
}
private async validateArgs(cliArgs: {}): Promise<void> {
if (this.options.validateArgs) return this.options.validateArgs(cliArgs)
}
private async preInstall(): Promise<void> {
if (this.options.preInstall) return this.options.preInstall()
}
private async beforeEach(stepId: string | number): Promise<void> {
if (this.options.beforeEach) return this.options.beforeEach(stepId)
}
private async afterEach(stepId: string | number): Promise<void> {
if (this.options.afterEach) return this.options.afterEach(stepId)
}
private async postInstall(): Promise<void> {
if (this.options.postInstall) return this.options.postInstall()
}
async displayFrontmatter() {
log.branded(`Welcome to the installer for ${this.options.packageName}`)
log.branded(this.options.packageDescription)
log.info(`This package is authored and supported by ${this.options.packageOwner}`)
log.info(`For additional documentation and support please visit ${this.options.packageRepoLink}`)
console.log()
await waitForConfirmation('Press enter to begin installation')
}
async run(cliArgs: {}): Promise<void> {
await this.displayFrontmatter()
try {
await this.validateArgs(cliArgs)
} catch (err) {
log.error(err)
return
}
await this.preInstall()
for (const step of this.steps) {
console.log() // newline
await this.beforeEach(step.stepId)
logExecutorFrontmatter(step)
// using if instead of a switch allows us to strongly type the executors
if (isFileTransformExecutor(step)) {
await fileTransformExecutor(step, cliArgs)
} else if (isAddDependencyExecutor(step)) {
await addDependencyExecutor(step, cliArgs)
} else if (isNewFileExecutor(step)) {
await newFileExecutor(step, cliArgs)
}
await this.afterEach(step.stepId)
}
await this.postInstall()
console.log()
log.success(`Installer complete, ${this.options.packageName} is now be configured for your app!`)
}
}

View File

@@ -0,0 +1,21 @@
import {ASTNode} from 'ast-types'
import {NamedTypes} from 'ast-types/gen/namedTypes'
import {builders} from 'ast-types/gen/builders'
import {types} from 'recast'
export function addImport(
ast: ASTNode,
__b: builders,
t: NamedTypes,
importToAdd: types.namedTypes.ImportDeclaration,
) {
if (!t.File.check(ast) || !t.ImportDeclaration.check(importToAdd)) return
const statements = ast.program.body
if (statements.length > 0 && !t.ImportDeclaration.check(statements[0])) {
ast.program.body.splice(0, 0, importToAdd)
} else {
const idx = ast.program.body.findIndex((node) => t.ImportDeclaration.check(node))
ast.program.body.splice(idx + 1, 0, importToAdd)
}
return ast
}

View File

@@ -0,0 +1 @@
export * from './add-import'

View File

@@ -0,0 +1,21 @@
import * as fs from 'fs-extra'
import * as path from 'path'
function ext(jsx = false) {
return fs.existsSync(path.resolve('tsconfig.json')) ? (jsx ? '.tsx' : '.ts') : '.js'
}
export const paths = {
document() {
return `app/pages/_document${ext(true)}`
},
app() {
return `app/pages/_app${ext(true)}`
},
entry() {
return `app/pages/index${ext(true)}`
},
blitzConfig() {
return 'blitz.config.js'
},
}

View File

@@ -0,0 +1,57 @@
import * as fs from 'fs-extra'
import {parse, print, types} from 'recast'
import {builders} from 'ast-types/gen/builders'
import {namedTypes, NamedTypes} from 'ast-types/gen/namedTypes'
import * as babel from 'recast/parsers/babel'
import getBabelOptions, {Overrides} from 'recast/parsers/_babel_options'
export const customTsParser = {
parse(source: string, options?: Overrides) {
const babelOptions = getBabelOptions(options)
babelOptions.plugins.push('typescript')
babelOptions.plugins.push('jsx')
return babel.parser.parse(source, babelOptions)
},
}
export enum TransformStatus {
Success = 'success',
Failure = 'failure',
}
export interface TransformResult {
status: TransformStatus
filename: string
error?: Error
}
export type Transformer = (ast: types.ASTNode, builder: builders, types: NamedTypes) => types.ASTNode
export function transform(transformerFn: Transformer, targetFilePaths: string[]): TransformResult[] {
const results: TransformResult[] = []
for (const filePath of targetFilePaths) {
if (!fs.existsSync(filePath)) {
results.push({
status: TransformStatus.Failure,
filename: filePath,
error: new Error(`Error: ${filePath} not found`),
})
}
try {
const fileBuffer = fs.readFileSync(filePath)
const fileSource = fileBuffer.toString('utf-8')
const ast = parse(fileSource, {parser: customTsParser})
const transformedCode = print(transformerFn(ast, types.builders, namedTypes)).code
fs.writeFileSync(filePath, transformedCode)
results.push({
status: TransformStatus.Success,
filename: filePath,
})
} catch (err) {
results.push({
status: TransformStatus.Failure,
filename: filePath,
error: err,
})
}
}
return results
}

View File

@@ -0,0 +1,12 @@
const defaultMessage = 'Press enter to continue'
export async function waitForConfirmation(message: string = defaultMessage) {
return new Promise((res) => {
process.stdin.resume()
process.stdout.write(message)
process.stdin.once('data', () => {
process.stdin.pause()
res()
})
})
}

View File

@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`addImport transform adds import at start of file with no imports present 1`] = `
"import React from \\"react\\";
export const truth = () => 42"
`;
exports[`addImport transform adds import at the end of all imports if imports are present 1`] = `
"import React from 'react'
import \\"app/styles/app.css\\";
export default function Comp() {
return <div>hello world!</div>
}"
`;

View File

@@ -0,0 +1,37 @@
import {addImport, customTsParser} from '@blitzjs/installer'
import {parse, print, types} from 'recast'
import {namedTypes} from 'ast-types/gen/namedTypes'
const b = types.builders
function executeImport(fileStr: string, importStatement: types.namedTypes.ImportDeclaration): string {
return print(
addImport(
parse(fileStr, {parser: customTsParser}) as types.namedTypes.File,
types.builders,
namedTypes,
importStatement,
) as types.namedTypes.File,
).code
}
describe('addImport transform', () => {
it('adds import at start of file with no imports present', () => {
const file = `export const truth = () => 42`
const importStatement = b.importDeclaration(
[b.importDefaultSpecifier(b.identifier('React'))],
b.literal('react'),
)
expect(executeImport(file, importStatement)).toMatchSnapshot()
})
it('adds import at the end of all imports if imports are present', () => {
const file = `import React from 'react'
export default function Comp() {
return <div>hello world!</div>
}`
const importStatement = b.importDeclaration([], b.literal('app/styles/app.css'))
expect(executeImport(file, importStatement)).toMatchSnapshot()
})
})

View File

@@ -0,0 +1,9 @@
{
"extends": "../tsconfig",
"compilerOptions": {
"noEmit": true,
"types": ["jest"],
"target": "es6"
},
"references": [{"path": ".."}]
}

View File

@@ -0,0 +1,23 @@
import {paths} from '@blitzjs/installer'
import * as fs from 'fs-extra'
jest.mock('fs-extra')
describe('path utils', () => {
it('returns proper file paths in a TS project', () => {
jest.spyOn(fs, 'existsSync').mockReturnValue(true)
expect(paths.document()).toBe('app/pages/_document.tsx')
expect(paths.app()).toBe('app/pages/_app.tsx')
expect(paths.entry()).toBe('app/pages/index.tsx')
// blitz config is always JS, we shouldn't transform this extension
expect(paths.blitzConfig()).toBe('blitz.config.js')
})
it('returns JS file paths in a JS project', () => {
jest.spyOn(fs, 'existsSync').mockReturnValue(false)
expect(paths.document()).toBe('app/pages/_document.js')
expect(paths.app()).toBe('app/pages/_app.js')
expect(paths.entry()).toBe('app/pages/index.js')
expect(paths.blitzConfig()).toBe('blitz.config.js')
})
})

View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "types", "test"],
"exclude": ["node_modules"],
"compilerOptions": {
"baseUrl": "./",
"declarationDir": "./dist",
"downlevelIteration": true,
"paths": {
"*": ["src/*", "node_modules/*"]
}
}
}

View File

@@ -84,6 +84,10 @@ const progress = (msg: string) => {
console.log(withCaret(chalk.bold(msg)))
}
const info = (msg: string) => {
console.log(chalk.bold(msg))
}
const spinner = (str: string) => {
return ora({
text: str,
@@ -128,4 +132,5 @@ export const log = {
spinner,
success,
variable,
info,
}

View File

@@ -2911,7 +2911,7 @@
"@types/node" "*"
form-data "^3.0.0"
"@types/node@*", "@types/node@>= 8", "@types/node@^13.13.2":
"@types/node@*", "@types/node@>= 8", "@types/node@^13.13.2", "@types/node@^13.13.4":
version "13.13.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.4.tgz#1581d6c16e3d4803eb079c87d4ac893ee7501c2c"
integrity sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==
@@ -3902,6 +3902,11 @@ ast-types-flow@0.0.7, ast-types-flow@^0.0.7:
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0=
ast-types@0.13.3, ast-types@^0.13.3:
version "0.13.3"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.3.tgz#50da3f28d17bdbc7969a3a2d83a0e4a72ae755a7"
integrity sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA==
astral-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
@@ -7781,6 +7786,18 @@ globalyzer@^0.1.0:
resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.4.tgz#bc8e273afe1ac7c24eea8def5b802340c5cc534f"
integrity sha512-LeguVWaxgHN0MNbWC6YljNMzHkrCny9fzjmEUdnF1kQ7wATFD1RHFRqA1qxaX2tgxGENlcxjOflopBwj3YZiXA==
globby@11.0.0, globby@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.0.tgz#56fd0e9f0d4f8fb0c456f1ab0dee96e1380bc154"
integrity sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg==
dependencies:
array-union "^2.1.0"
dir-glob "^3.0.1"
fast-glob "^3.1.1"
ignore "^5.1.4"
merge2 "^1.3.0"
slash "^3.0.0"
globby@^10.0.1:
version "10.0.2"
resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.2.tgz#277593e745acaa4646c3ab411289ec47a0392543"
@@ -7795,18 +7812,6 @@ globby@^10.0.1:
merge2 "^1.2.3"
slash "^3.0.0"
globby@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.0.tgz#56fd0e9f0d4f8fb0c456f1ab0dee96e1380bc154"
integrity sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg==
dependencies:
array-union "^2.1.0"
dir-glob "^3.0.1"
fast-glob "^3.1.1"
ignore "^5.1.4"
merge2 "^1.3.0"
slash "^3.0.0"
globby@^9.2.0:
version "9.2.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d"
@@ -13061,6 +13066,16 @@ realpath-native@^1.1.0:
dependencies:
util.promisify "^1.0.0"
recast@^0.19.1:
version "0.19.1"
resolved "https://registry.yarnpkg.com/recast/-/recast-0.19.1.tgz#555f3612a5a10c9f44b9a923875c51ff775de6c8"
integrity sha512-8FCjrBxjeEU2O6I+2hyHyBFH1siJbMBLwIRvVr1T3FD2cL754sOaJDsJ/8h3xYltasbJ8jqWRIhMuDGBSiSbjw==
dependencies:
ast-types "0.13.3"
esprima "~4.0.0"
private "^0.1.8"
source-map "~0.6.1"
rechoir@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
@@ -15088,7 +15103,7 @@ ts-jest@24.3.0, ts-jest@^24.0.2:
semver "^5.5"
yargs-parser "10.x"
ts-node@^8.9.0:
ts-node@^8.9.0, ts-node@^8.9.1:
version "8.9.1"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.9.1.tgz#2f857f46c47e91dcd28a14e052482eb14cfd65a5"
integrity sha512-yrq6ODsxEFTLz0R3BX2myf0WBCSQh9A+py8PBo1dCzWIOcvisbyH6akNKqDHMgXePF2kir5mm5JXJTH3OUJYOQ==