Skip to content

Instantly share code, notes, and snippets.

@aldonline
Last active March 30, 2021 21:27
Show Gist options
  • Save aldonline/7cd8103f9005df3eccb0497b79e36380 to your computer and use it in GitHub Desktop.
Save aldonline/7cd8103f9005df3eccb0497b79e36380 to your computer and use it in GitHub Desktop.
import * as fs from "fs-extra"
import { ensureDirSync } from "fs-extra"
import { LazyGetter as lazy } from "lazy-get-decorator"
import { Memoize as memo } from "lodash-decorators"
import { dirname, join } from "path"
import vscode from "vscode"
import { degit_with_retries } from "../../../degit/degit_with_retries"
import { GitURL } from "../../../git/GitURL"
import { TargetDirSpecification } from "../../util/TargetDirSpecification"
import { TargetDirSpecification_resolve_vsc } from "../../util/TargetDirSpecification_resolve_vsc"
import { vscode_run } from "../../../vscode/vscode_run"
interface Opts {
gitUrl: GitURL
/**
* will use npx degit instead of git lone (must faster, but disconnects from repo)
*/
degit?: boolean
targetDir: TargetDirSpecification
}
export function clone_repo(opts: Opts) {
return new CloneRepo(opts).clone()
}
export function clone_repo_dry(opts: Opts) {
return new CloneRepo(opts).clone_dry()
}
class CloneRepo {
constructor(private opts: Opts) {}
@memo() async start(): Promise<vscode.Uri | undefined> {
// TODO: add more checks
// for now we just try to git clone
return await this.clone_withProgress()
}
@memo() private async resolvedTargetDir() {
const { targetDir } = this.opts
return await TargetDirSpecification_resolve_vsc({
targetDir,
autoNamePrefix: this.opts.gitUrl.name,
})
}
@memo() async clone_withProgress() {
const { repo_url } = this
const destFolder = await this.resolvedTargetDir()
if (!destFolder) return
return await vscode.window.withProgress<vscode.Uri | undefined>(
{
location: vscode.ProgressLocation.Notification,
title: `cloning ${repo_url} into ${destFolder}`,
},
() => this.clone()
)
}
@memo() async clone(): Promise<vscode.Uri | undefined> {
const { repo_url } = this
const destFolder = await this.resolvedTargetDir()
if (!destFolder) return
if (!repo_url) return
await actual_clone(repo_url, destFolder, this.opts.degit)
return vscode.Uri.file(destFolder)
}
@memo() async clone_dry(): Promise<string | undefined> {
const { repo_url } = this
const destFolder = await this.resolvedTargetDir()
if (!destFolder) return
ensureDirSync(destFolder)
if (this.opts.degit) {
return `npx degit ${repo_url} ${destFolder}`
}
return `git clone ${repo_url} ${destFolder}`
}
@lazy() get repo_url(): string | undefined {
return this.opts.gitUrl.raw
}
}
async function actual_clone(repo: string, dest: string, degit?: boolean) {
ensureDirSync(dirname(dest))
if (degit) {
try {
// fastest way to clone and degit is to use "degit"
await degit_with_retries(repo, dest)
} catch (e) {
await vscode_run({ cmd: `git clone --depth 1 ${repo} ${dest}` })
const dotgit = join(dest, ".git")
if (fs.existsSync(dotgit)) await fs.remove(dotgit)
}
} else {
// otherwise just git clone
await vscode_run({ cmd: `git clone ${repo} ${dest}` })
// await simple_git(destFolder).clone(repo_url!, destFolder)
}
}
import { existsSync, removeSync } from "fs-extra"
import { values } from "lodash"
import { Memoize as memo } from "lodash-decorators"
import { join } from "path"
import vscode, { Uri } from "vscode"
import { Command } from "vscode-languageserver-types"
import { jamstackide_dev_animation_open } from "../../../../vsc_jamstack_ide/components/dev_animation/jamstackide_dev_animation_open"
import { GitURL } from "../../../git/GitURL"
import { npm__yarn__install_dry } from "../../../npm__yarn/npm__yarn__install"
import { wait } from "../../../Promise/wait"
import { shell_wrapper_run_or_fail } from "../../../vscode/Terminal/shell_wrapper/shell_wrapper_run"
import { vscode_run } from "../../../vscode/vscode_run"
import { vscode_window_createTerminal_andRun } from "../../../vscode/vscode_window_createTerminal_andRun"
import {
NewJamstackProjectSource,
NewJamstackProjectSourceString,
NewJamstackProjectSource_autoPickDir,
NewJamstackProjectSource_parse,
} from "../../util/NewJamstackProjectSource"
import { NewJamstackProjectSource_prompt } from "../../util/NewJamstackProjectSource_prompt"
import { TargetDirSpecification } from "../../util/TargetDirSpecification"
import { clone_repo } from "./clone_repo"
import { init_hook_set_and_open } from "./init_hook"
import { jamstack_projects_dir } from "./jamstack_projects_dir"
import { start_dev } from "./start_dev"
import { yarn_create_dry } from "./yarn_create"
export const commands = {
// this is a public facing command
develop_locally: {
command: "decoupled.jamstackide.develop_locally",
title: "Fetch and Develop Locally",
category: "Jamstack",>
},
_sandbox: {
command: "decoupled.jamstackide.sandbox",
title: "Sandbox (remove before release)",
category: "Jamstack",
},
}
export function ___buildmeta___() {
return {
pjson: {
contributes: {
commands: [...values(commands)],
},
},
}
}
export type Opts =
| FromMagicURL
| InitAfterReload
| FromCommandInvocation
| FromNetlifyExplorer
interface FromMagicURL {
action: "FromMagicURL"
source?: NewJamstackProjectSourceString
extraOpts?: ExtraOpts
}
export interface ExtraOpts {
/**
* relative path to a file to open upon launching
*/
open?: string
/**
* override netlify-dev framework
*/
framework?: string
/**
* provide a command run start dev
* TODO: prompt user for authorization
*/
command?: string
/**
* override the install command
* TODO: prompt user for authorization
*/
install?: string
/**
* use degit instead of git clone
*/
degit?: boolean
}
interface FromNetlifyExplorer {
action: "FromNetlifyExplorer"
source: NewJamstackProjectSourceString
}
export interface FromCommandInvocation {
action: "FromCommandInvocation"
source?: NewJamstackProjectSourceString
}
interface InitAfterReload {
action: "InitAfterReload"
source: NewJamstackProjectSourceString
workspaceUri: string
extraOpts?: ExtraOpts
}
export function develop_locally(opts: Opts, ctx: vscode.ExtensionContext) {
return new DevelopLocally(opts, ctx).start()
}
class DevelopLocally {
constructor(private opts: Opts, private ctx: vscode.ExtensionContext) {}
@memo() async start() {
const opts = this.opts
const { ctx } = this
if (opts.action === "FromCommandInvocation") {
const source = await NewJamstackProjectSource_prompt()
if (!source) return
reload_and_init({ source, openInNewWindow: true, ctx })
return
}
if (opts.action === "FromNetlifyExplorer") {
const source = opts.source
? NewJamstackProjectSource_parse(opts.source)
: await NewJamstackProjectSource_prompt()
if (!source) return
reload_and_init({ source, openInNewWindow: true, ctx })
return
}
if (opts.action === "FromMagicURL") {
const source = opts.source
? NewJamstackProjectSource_parse(opts.source)
: await NewJamstackProjectSource_prompt()
if (!source) return
const wfs = vscode.workspace.workspaceFolders ?? []
const openInNewWindow = wfs.length > 0
reload_and_init({
source,
openInNewWindow,
ctx,
extraOpts: opts.extraOpts,
})
return
}
if (opts.action === "InitAfterReload") {
const { workspaceUri, extraOpts } = opts
const wf = requireAtLeastOneOpenWorkspace(workspaceUri)
const source = NewJamstackProjectSource_parse(opts.source)
hideAll()
// we should also make sure the panel and the terminals are closed
// workbench.action.terminal.toggleTerminal
// open animation (for now this is totally disconnected)
jamstackide_dev_animation_open(this.ctx)
const targetDir: TargetDirSpecification = {
kind: "specific",
dir: wf.uri.fsPath,
}
// fetch code
// delete the .jamstackide folder if present
// otherwise "git clone" and "yarn create" won't work
removeSync(join(wf.uri.fsPath, ".jamstackide"))
if (source instanceof GitURL) {
const clone_opts = {
gitUrl: source,
targetDir,
degit: extraOpts?.degit,
}
await clone_repo(clone_opts)
// const rr = await clone_repo_dry(clone_opts)
// if (!rr) return
// await run({ cmd: rr })
// await jamstackide_shell_wrapper_run_or_fail(rr, cmd => {
// vscode_window_createTerminal_andRun({ cmd })
// })
//await npm__yarn__install(wf.uri.fsPath)
const ok = await install_deps({ dir: wf.uri.fsPath, extraOpts })
if (!ok) return
} else {
// yarn create
const opts = {
packageName: source,
targetDir,
}
//await yarn_create(opts)
const rr = await yarn_create_dry(opts)
if (!rr) return
const cmdstr2 = rr.cmd + " " + rr.dest
await vscode_run({ cmd: cmdstr2 })
}
restartEverything()
// start dev
await start_dev({ uri: wf.uri, ctx: this.ctx, extraOpts })
return
}
}
}
async function restartEverything() {
// PERFORMANCE
// performance can be sluggish at this point
// (many files changed, some language servers are going nuts at this point)
// restart ts server - this seems to help
// if performance becomes an issue at this point, we could also restart the extension host
// this would restart *this* extension, but with some trickery we could pull it off
// workbench.action.restartExtensionHost
try {
await vscode.commands.executeCommand("typescript.restartTsServer")
} catch (e) {}
}
async function install_deps_dry(opts: {
dir: string
extraOpts?: ExtraOpts
}): Promise<string[] | undefined> {
const { extraOpts, dir } = opts
if (extraOpts?.install) {
const install_cmd = extraOpts.install
// some known commands are whitelisted
if (install_cmd === "bundle install") {
// https://jekyllrb.com/tutorials/using-jekyll-with-bundler/
// install dependencies locally to avoid permission issues
// TODO: add this line to .gitignore
return bundle_install_cmd()
} else {
vscode.window.showWarningMessage(
`custom install commands not implemented yet: ${install_cmd}`
)
return
}
} else {
// guess
const gemfile = join(dir, "Gemfile")
if (existsSync(gemfile)) {
return bundle_install_cmd()
}
const npmi = await npm__yarn__install_dry(dir)
return npmi ? [npmi] : undefined
}
}
function bundle_install_cmd() {
// TODO: check for bundle installation
const line1 = `bundle config set --local path '.vendor/bundle'`
const line2 = "bundle install"
return [line1, line2]
}
async function install_deps(opts: { dir: string; extraOpts?: ExtraOpts }) {
const cmds = await install_deps_dry(opts)
if (!cmds) return false
for (const cmd of cmds)
await shell_wrapper_run_or_fail(cmd, cmd => {
vscode_window_createTerminal_andRun({ cmd, cwd: opts.dir })
})
return true
}
async function reload_and_init({
source,
dir,
openInNewWindow,
ctx,
extraOpts,
}: {
source: NewJamstackProjectSource
dir?: string
openInNewWindow?: boolean
ctx: vscode.ExtensionContext
extraOpts?: ExtraOpts
}) {
const dev = ctx.extensionMode === vscode.ExtensionMode.Development
if (dev) {
openInNewWindow = false // otherwise we would get a window with no extension
// close any open workspace folders and wait a bit
const wfs = vscode.workspace.workspaceFolders
if (wfs && wfs.length > 0) {
vscode.workspace.updateWorkspaceFolders(0, wfs?.length)
await wait(1000)
}
}
const dir2 =
dir ?? NewJamstackProjectSource_autoPickDir(source, jamstack_projects_dir())
const cmd = {
command: commands.develop_locally.command,
arguments: [
{
action: "InitAfterReload",
source: source.raw,
workspaceUri: Uri.file(dir2).toString(),
extraOpts,
} as InitAfterReload,
],
title: "init",
} as Command
init_hook_set_and_open(dir2, cmd, openInNewWindow)
}
// function devLaunchMarkerForDir(dir: string): string {
// return join(dir, ".jamstackide", ".dev")
// }
function requireAtLeastOneOpenWorkspace(uri: string) {
const wf = vscode.workspace.workspaceFolders?.find(
wf => wf.uri.toString() === uri
)
if (!wf) throw new Error(`workspace is not currently open: ${uri}`)
return wf
}
export function hideAll() {
vscode.commands.executeCommand("workbench.action.closeAllEditors")
vscode.commands.executeCommand("workbench.action.closePanel")
vscode.commands.executeCommand("workbench.action.closeSidebar")
}
/* vscode.window.state.focused
{ "key": "ctrl+cmd+f", "command": "workbench.action.toggleFullScreen" },
{ "key": "cmd+j", "command": "workbench.action.togglePanel" },
{ "key": "cmd+b", "command": "workbench.action.toggleSidebarVisibility" },
*/
import { ResolvedNetlifyDevSettings } from "./types"
import execa from "execa"
export async function netlify_dev_dry_settings(
dir: string
): Promise<ResolvedNetlifyDevSettings | undefined> {
const collected = await netlify_dev_dry(dir)
for (const c of collected) if (c.type === "settings") return c.data
}
async function netlify_dev_dry(dir: string) {
// this requires @decoupled/netlify-cli@2.59.1-alpha.3 to be installed globally
//const netlify_2 = "/Users/aldo/com.github/decoupled/netlify-cli/bin/run"
const netlify_2 = "decoupled-netlify"
const dev_dry = `${netlify_2} dev --xdry`
const cwd = dir
const [cmd, ...args] = dev_dry.split(" ")
const res = await execa(cmd, args, { cwd })
if (res.exitCode === 1) {
}
const collected: any[] = []
const parts = res.stdout.split(start_delim)
for (const part of parts) {
if (part.includes(end_delim)) {
const pp = part.split(end_delim)
collected.push(JSON.parse(pp[0]))
}
}
collected //?
return collected
}
// these are copy pasted into the "hacked" netlify-cli
const start_delim = "----start-----decoupled-delimiter----78978978e979---2----"
const end_delim = "----end-----decoupled-delimiter----78978978e979---2----"
interface DetectionOutput {
netlifyDev?: NetlifyDevDetectionOutput
}
interface NetlifyDevDetectionOutput {}
import command_exists from "command-exists"
import { existsSync } from "fs-extra"
import { join } from "path"
import { getPortPromise } from "portfinder"
import vscode from "vscode"
import waitPort from "wait-port"
import { netlify_dev_dry_settings } from "../../../netlify/dev/netlify_dev_dry_run"
import { wait } from "../../../Promise/wait"
// import { jamstackide_vsc_treeview_get } from "../treeview/jamstackide_vsc_treeview"
import { browser_preview } from "./browser_preview"
import { ExtraOpts } from "./develop_locally"
import { vscode_run } from "../../../vscode/vscode_run"
import { gatsby_wait_for_dev_server_ready } from "../../../gatsby/gatsby_wait_for_dev_server_ready"
interface Opts {
uri: vscode.Uri
ctx: vscode.ExtensionContext
extraOpts?: ExtraOpts
}
export async function start_dev(opts: Opts) {
const { uri, ctx, extraOpts } = opts
// focus on the explorer view
vscode.commands.executeCommand("workbench.view.explorer")
// try to focus the jamstack treeview
// jamstackide_vsc_treeview_get(ctx).reveal()
const cmds: string[] = []
// analyze
const settings = await netlify_dev_dry_settings(uri.fsPath)
let use_netlify_dev = true
let port = await getPortPromise()
const netlify_dev_flags: string[] = []
if (extraOpts?.framework === "redwood") {
use_netlify_dev = false
false &&
netlify_dev_flags.push(
"--command='yarn rw dev' --framework='#custom' --targetPort=8910"
)
cmds.push(`yarn rw dev --fwd="--port=${port} --open=false"`)
//port = 8910
} else if (settings?.framework === "gatsby") {
use_netlify_dev = false
cmds.push("yarn gatsby develop -p " + port)
//cmds.push(`netlify dev -p `)
// for now we're not using netlify dev
// we have no way of overriding targetPort cleanly
}
// we will specify a port for netlify dev
// -c, --command=command command to run
// -f, --functions=functions Specify a functions folder to serve
// -o, --offline disables any features that require network access
// -p, --port=port Specify port of netlify dev
// -l, --live Start a public live session
// const flags: string[] = []
// if (overrideCommand) flags.push(`-c '${overrideCommand}'`)
if (use_netlify_dev) {
netlify_dev_flags.push("-p " + port)
const has_netlify = command_exists.sync("netlify")
if (!has_netlify) cmds.push("npm i -g netlify-cli")
cmds.push(["netlify dev", ...netlify_dev_flags].join(" "))
}
const runn = async () => {
for (const cmd of cmds)
await vscode_run({ cmd, name: "Jamstack Dev", cwd: uri.fsPath })
}
runn()
const urlll = `http://localhost:${port}/`
await waitPort({ port })
if (settings?.framework === "gatsby") {
// gatsby develop will start a server that keeps http requests open
// during startup. It can take 10, 20 seconds
// so we have a special heuristic for this case
await wait(3000)
await gatsby_wait_for_dev_server_ready({ port })
await wait(1000)
} else {
// give it some time anyway.
// the vscode browser preview will fail if the HTTP request is not fulfilled immediately
await wait(3000)
}
// TODO
// const app = express()
// app.use(bodyParser.json())
// app.use(cors())
// app.post("/browser-route-change", (req, res) => {
// const location = req.body
// })
// app.listen(6734)
await browser_preview(urlll)
//open(urlll)
// open file
openFile(opts)
}
async function openFile({ extraOpts, uri }: Opts) {
const candidates1 = ["pages/index.js", "src/pages/index.js", "src/index.js"]
if (extraOpts?.open) {
candidates1.unshift(extraOpts.open)
}
const candidates = candidates1.map(x => join(uri.fsPath, x))
const ff = candidates.find(x => {
return existsSync(x.split(":")[0])
})
if (ff) {
const [file, line, col] = ff.split(":")
const selection = parseRange(line, col)
const uri = vscode.Uri.file(file)
await vscode.workspace.openTextDocument(uri)
await vscode.window.showTextDocument(uri, { selection })
}
}
function parseRange(
line: string = "",
col: string = ""
): vscode.Range | undefined {
const lineN = parseInt(line)
if (isNaN(lineN)) return
const colN = isNaN(parseInt(col)) ? 0 : parseInt(col)
const pp = new vscode.Position(lineN, colN)
return new vscode.Range(pp, pp)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment