Last active
March 30, 2021 21:27
-
-
Save aldonline/7cd8103f9005df3eccb0497b79e36380 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" }, | |
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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