This repository has been archived on 2025-12-11. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
fullstack-open/part3/phonebook/node_modules/@flydotio/dockerfile/gdf.js
Andrew Trieu fa5d8255d6 Add part3
2023-05-23 08:05:53 +03:00

418 lines
11 KiB
JavaScript
Executable File

#!/usr/bin/env node
import fs from 'node:fs'
import url from 'node:url'
import path from 'node:path'
import { execSync } from 'node:child_process'
import * as ejs from 'ejs'
import chalk from 'chalk'
import * as Diff from 'diff'
import * as readline from 'node:readline'
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
// Generate Dockerfile class
export class GDF {
static templates = path.join(__dirname, 'templates')
// Where the app is. Used both for scanning and is updated with new files.
#appdir
// Parsed package.json file contents.
#pj
// which packager is used (npm, pnpm, yarn)
#packager
// previous answer to conflict prompt
#answer = ''
// Does this application use remix.run?
get remix() {
return !!(this.#pj.dependencies?.remix ||
this.#pj.dependencies?.['@remix-run/node'])
}
// Does this application use prisma?
get prisma() {
return !!(this.#pj.dependencies?.['@prisma/client'] ||
this.#pj.devDependencies?.prisma)
}
// Does this application use next.js?
get nextjs() {
return !!this.#pj.dependencies?.next
}
// Does this application use nuxt.js?
get nuxtjs() {
return !!this.#pj.dependencies?.nuxt
}
// Does this application use gatsby?
get gatsby() {
return !!this.#pj.dependencies?.gatsby
}
// Does this application use nest?
get nestjs() {
return !!this.#pj.dependencies?.['@nestjs/core']
}
// what node version should be used?
get nodeVersion() {
const ltsVersion = '18.16.0'
try {
return execSync('node -v', { encoding: 'utf8' })
.match(/\d+\.\d+\.\d+/)?.[0] || ltsVersion
} catch {
return ltsVersion
}
}
// classic version of yarn (installed by default)
yarnClassic = '1.22.19'
// What yarn version should be used?
get yarnVersion() {
const version = this.#pj.packageManager?.match(/(\d+\.\d+\.\d+)/)?.[0] // Should return something like "1.22.10"
if (version !== undefined) {
return version
} else {
try {
return execSync('yarn --version', { encoding: 'utf8' })
.match(/\d+\.\d+\.\d+/)?.[0] || this.yarnClassic
} catch {
return this.yarnClassic
}
}
}
// What pnpm version should be used?
get pnpmVersion() {
try {
return execSync('pnpm --version', { encoding: 'utf8' })
.match(/\d+\.\d+\.\d+/)?.[0] || 'latest'
} catch {
return 'latest'
}
}
// List of package files needed to install
get packageFiles() {
const result = ['package.json']
for (const file of ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock']) {
if (fs.statSync(path.join(this.#appdir, file), { throwIfNoEntry: false })) {
result.push(file)
}
}
return result
}
// Which packager should be used?
get packager() {
if (this.#packager !== undefined) return this.#packager
const packageFiles = this.packageFiles
if (packageFiles.includes('yarn.lock')) {
this.#packager = 'yarn'
} else if (packageFiles.includes('pnpm-lock.yaml')) {
this.#packager = 'pnpm'
} else {
this.#packager = 'npm'
}
return this.#packager
}
// install all dependencies in package.json
get packagerInstall() {
let install = `${this.packager} install`
const packageFiles = this.packageFiles
// clean install
if (this.packager === 'npm' && packageFiles.includes('package-lock.json')) {
install = 'npm ci'
} else if (packageFiles.includes('yarn.lock')) {
if (this.yarnVersion.startsWith('1.')) {
install += ' --frozen-lockfile'
} else if (this.yarnVersion.startsWith('2.')) {
install += ' --immutable --immutable-cache --check-cache'
} else {
// yarn 3+
install += ' --immutable'
}
} else if (packageFiles.includes('pnpm-lock.yaml')) {
install += ' --frozen-lockfile'
}
// optionally include dev dependencies
if (this.devDependencies && !this.pnpm) {
if (this.yarn) {
install += ' --production=false'
} else {
install += ' --include=dev'
}
}
// optionally include legacy peer dependencies
if (this.options.legacyPeerDeps) {
if (this.npm) {
install += ' --legacy-peer-deps'
} else if (this.yarn && !this.yarnVersion.startsWith('1.')) {
install += ' --legacy-peer-deps'
}
}
// optionally include scripts
if (this.options.ignoreScripts) {
install += ' --ignore-scripts'
}
return install
}
// Prune development dependencies
get packagerPrune() {
let prune
if (this.yarn) {
prune = 'yarn install --production=true'
if (this.options.legacyPeerDeps && !this.yarnVersion.startsWith('1.')) {
prune += ' --legacy-peer-deps'
}
} else if (this.pnpm) {
prune = 'pnpm prune --prod'
} else {
prune = 'npm prune --omit=dev'
if (this.options.legacyPeerDeps) prune += ' --legacy-peer-deps'
}
return prune
}
// Is the packager yarn?
get yarn() {
return this.packager === 'yarn'
}
// Is the packager npm?
get npm() {
return this.packager === 'npm'
}
// Is the packager pnpm?
get pnpm() {
return this.packager === 'pnpm'
}
// How to install python (switched from buster to bullseye)
get python() {
return parseInt(this.nodeVersion.split('.')[0]) > 16 ? 'python-is-python3' : 'python'
}
// Are there any development dependencies?
get devDependencies() {
return !!this.#pj.devDependencies
}
// Is there a build script?
get build() {
return !!this.#pj.scripts?.build
}
// Descriptive form of detected runtime
get runtime() {
let runtime = 'Node.js'
if (this.remix) runtime = 'Remix'
if (this.nextjs) runtime = 'Next.js'
if (this.nuxtjs) runtime = 'Nuxt.js'
if (this.nestjs) runtime = 'NestJS'
if (this.gatsby) runtime = 'Gatsby'
if (this.prisma) runtime += '/Prisma'
return runtime
}
get user() {
return this.runtime.split('/')[0].replaceAll('.', '').toLowerCase()
}
// command to start the web server
get startCommand() {
if (this.gatsby) {
return ['npx', 'gatsby', 'serve', '-H', '0.0.0.0']
} else if (this.runtime === 'Node.js' && this.#pj.scripts?.start?.includes('fastify')) {
let start = this.#pj.scripts.start
if (!start.includes('-a') && !start.includes('--address')) {
start = start.replace('start', 'start --address 0.0.0.0')
}
start = start.split(' ')
start.unshift('npx')
return start
} else {
return [this.packager, 'run', 'start']
}
}
// Does this Dockerfile need an entrypoint script?
get entrypoint() {
return this.prisma || this.options.swap
}
// determine if the entrypoint needs to be adjusted to run on Linux
// generally only needed when developing on windows
get entrypointFixups() {
const fixups = []
const entrypoint = path.join(this.#appdir, 'docker-entrypoint')
const stat = fs.statSync(entrypoint, { throwIfNoEntry: false })
if (!stat) return fixups
if (this.options.windows || !(stat.mode & fs.constants.S_IXUSR)) {
fixups.push('chmod +x ./docker-entrypoint')
}
if (this.options.windows || fs.readFileSync(entrypoint, 'utf-8').includes('\r')) {
fixups.push('sed -i "s/\\r$//g" ./docker-entrypoint')
}
return fixups
}
// Port to be used
get port() {
let port = 3000
if (this.gatsby) port = 8080
if (this.remix) port = 8080
return port
}
// render each template and write to the destination dir
async run(appdir, options = {}) {
this.options = options
this.#appdir = appdir
this.#pj = JSON.parse(fs.readFileSync(path.join(appdir, 'package.json'), 'utf-8'))
if (options.force) this.#answer = 'a'
// select and render templates
const templates = ['Dockerfile.ejs']
if (this.entrypoint) templates.unshift('docker-entrypoint.ejs')
for (const template of templates) {
const dest = await this.#writeTemplateFile(template)
if (template === 'docker-entrypoint.ejs') fs.chmodSync(dest, 0o755)
}
// ensure that there is a dockerignore file
try {
fs.statSync(path.join(appdir, '.dockerignore'))
} catch {
try {
fs.copyFileSync(
path.join(appdir, '.gitignore'),
path.join(appdir, '.dockerignore')
)
} catch {
await this.#writeTemplateFile('.dockerignore.ejs')
}
}
}
// write template file, prompting when there is a conflict
async #writeTemplateFile(template) {
const proposed = await ejs.renderFile(path.join(GDF.templates, template), this)
const name = template.replace(/\.ejs$/m, '')
const dest = path.join(this.#appdir, name)
if (fs.statSync(dest, { throwIfNoEntry: false })) {
const current = fs.readFileSync(dest, 'utf-8')
if (current === proposed) {
console.log(` ${chalk.bold.blue('identical')} ${name}`)
return dest
}
let prompt
let question
try {
if (this.#answer !== 'a') {
console.log(`${chalk.bold.red('conflict'.padStart(11))} ${name}`)
prompt = readline.createInterface({
input: process.stdin,
output: process.stdout
})
// support node 16 which doesn't have a promisfied readline interface
question = query => {
return new Promise(resolve => {
prompt.question(query, resolve)
})
}
}
while (true) {
if (question) {
this.#answer = await question(`Overwrite ${dest}? (enter "h" for help) [Ynaqdh] `)
}
switch (this.#answer.toLocaleLowerCase()) {
case '':
case 'y':
case 'a':
console.log(`${chalk.bold.yellow('force'.padStart(11, ' '))} ${name}`)
fs.writeFileSync(dest, proposed)
return dest
case 'n':
console.log(`${chalk.bold.yellow('skip'.padStart(11, ' '))} ${name}`)
return dest
case 'q':
process.exit(0)
break
case 'd':
console.log(Diff.createPatch(name, current, proposed, 'current', 'proposed').trimEnd() + '\n')
break
default:
console.log(' Y - yes, overwrite')
console.log(' n - no, do not overwrite')
console.log(' a - all, overwrite this and all others')
console.log(' q - quit, abort')
console.log(' d - diff, show the differences between the old and the new')
console.log(' h - help, show this help')
}
}
} finally {
if (prompt) prompt.close()
}
} else {
console.log(`${chalk.bold.green('create'.padStart(11, ' '))} ${name}`)
fs.writeFileSync(dest, proposed)
}
return dest
}
}