|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Atom++ — Hackability: package generator + hot reload |
| 3 | + * |
| 4 | + * "Generate Package" scaffolds a runnable Atom++ package (plain JS, no build step). |
| 5 | + * "Develop Package (Hot Reload)" loads a package's extension.js into the RUNNING window and |
| 6 | + * re-runs activate() on every save — Atom-style live development, no window restart. (Code |
| 7 | + * runs live; manifest-only `contributes` still need the generated F5 launch config.) |
| 8 | + *--------------------------------------------------------------------------------------------*/ |
| 9 | +// @ts-check |
| 10 | +'use strict'; |
| 11 | + |
| 12 | +const vscode = require('vscode'); |
| 13 | +const os = require('os'); |
| 14 | +const fs = require('fs'); |
| 15 | +const path = require('path'); |
| 16 | +const Module = require('module'); |
| 17 | + |
| 18 | +const PACKAGES_DIR = path.join(os.homedir(), '.atom-plus-plus', 'packages'); |
| 19 | + |
| 20 | +/** @type {vscode.OutputChannel} */ |
| 21 | +let output; |
| 22 | +/** Active dev session, or null. */ |
| 23 | +let dev = null; // { dir, name, exports, ctx, watcher, status, timer } |
| 24 | + |
| 25 | +// ---- generator ------------------------------------------------------------- |
| 26 | + |
| 27 | +function pkgJson(name, title) { |
| 28 | + return JSON.stringify({ |
| 29 | + name, |
| 30 | + displayName: title, |
| 31 | + description: 'An Atom++ package.', |
| 32 | + version: '0.0.1', |
| 33 | + publisher: 'you', |
| 34 | + engines: { vscode: '^1.126.0' }, |
| 35 | + categories: ['Other'], |
| 36 | + activationEvents: ['onStartupFinished'], |
| 37 | + main: './extension.js', |
| 38 | + contributes: { commands: [{ command: name + '.hello', title: title + ': Hello' }] } |
| 39 | + }, null, 2) + '\n'; |
| 40 | +} |
| 41 | + |
| 42 | +function extJs(name) { |
| 43 | + return `// ${name} — an Atom++ package. |
| 44 | +// |
| 45 | +// Develop it live: run "Atom++: Develop Package (Hot Reload)" and pick this folder. |
| 46 | +// Edit + save and activate() re-runs instantly — no window restart. (For manifest |
| 47 | +// contributions like keybindings, press F5 for a full Extension Development Host.) |
| 48 | +
|
| 49 | +const vscode = require('vscode'); |
| 50 | +
|
| 51 | +function activate(context) { |
| 52 | +\tconst hello = vscode.commands.registerCommand('${name}.hello', () => { |
| 53 | +\t\tvscode.window.showInformationMessage('Hello from ${name}! 👋'); |
| 54 | +\t}); |
| 55 | +\tcontext.subscriptions.push(hello); |
| 56 | +
|
| 57 | +\t// A status bar item — try changing this text and saving while hot-reload is running: |
| 58 | +\tconst item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0); |
| 59 | +\titem.text = '$(rocket) ${name}'; |
| 60 | +\titem.tooltip = 'From the ${name} package'; |
| 61 | +\titem.show(); |
| 62 | +\tcontext.subscriptions.push(item); |
| 63 | +} |
| 64 | +
|
| 65 | +function deactivate() {} |
| 66 | +
|
| 67 | +module.exports = { activate, deactivate }; |
| 68 | +`; |
| 69 | +} |
| 70 | + |
| 71 | +function launchJson() { |
| 72 | + return JSON.stringify({ |
| 73 | + version: '0.2.0', |
| 74 | + configurations: [{ |
| 75 | + name: 'Run Package', |
| 76 | + type: 'extensionHost', |
| 77 | + request: 'launch', |
| 78 | + args: ['--extensionDevelopmentPath=${workspaceFolder}'] |
| 79 | + }] |
| 80 | + }, null, 2) + '\n'; |
| 81 | +} |
| 82 | + |
| 83 | +async function generatePackage() { |
| 84 | + const name = await vscode.window.showInputBox({ |
| 85 | + title: 'Atom++ — Generate Package', |
| 86 | + prompt: 'Package name (lowercase, hyphens — e.g. my-cool-package)', |
| 87 | + validateInput: (v) => /^[a-z][a-z0-9-]*$/.test(v || '') ? null : 'Use lowercase letters, digits and hyphens; start with a letter.' |
| 88 | + }); |
| 89 | + if (!name) { return; } |
| 90 | + const dir = path.join(PACKAGES_DIR, name); |
| 91 | + if (fs.existsSync(dir)) { |
| 92 | + vscode.window.showErrorMessage('Atom++: a package named “' + name + '” already exists at ' + dir); |
| 93 | + return; |
| 94 | + } |
| 95 | + const title = name.replace(/(^|-)([a-z])/g, (_, s, c) => (s ? ' ' : '') + c.toUpperCase()); |
| 96 | + try { |
| 97 | + fs.mkdirSync(path.join(dir, '.vscode'), { recursive: true }); |
| 98 | + fs.writeFileSync(path.join(dir, 'package.json'), pkgJson(name, title), 'utf8'); |
| 99 | + fs.writeFileSync(path.join(dir, 'extension.js'), extJs(name), 'utf8'); |
| 100 | + fs.writeFileSync(path.join(dir, 'README.md'), '# ' + title + '\n\nAn Atom++ package.\n', 'utf8'); |
| 101 | + fs.writeFileSync(path.join(dir, '.vscode', 'launch.json'), launchJson(), 'utf8'); |
| 102 | + } catch (e) { |
| 103 | + vscode.window.showErrorMessage('Atom++: could not scaffold package: ' + ((e && e.message) || e)); |
| 104 | + return; |
| 105 | + } |
| 106 | + const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(path.join(dir, 'extension.js'))); |
| 107 | + await vscode.window.showTextDocument(doc); |
| 108 | + const go = 'Develop (Hot Reload)'; |
| 109 | + const c = await vscode.window.showInformationMessage('Atom++: created package “' + name + '”.', go); |
| 110 | + if (c === go) { startDev(dir); } |
| 111 | +} |
| 112 | + |
| 113 | +// ---- hot-reload dev loader ------------------------------------------------- |
| 114 | + |
| 115 | +/** Load a package's main module fresh, with require('vscode') wired to the real API. */ |
| 116 | +function loadPackageExports(dir) { |
| 117 | + let main = path.join(dir, 'extension.js'); |
| 118 | + try { |
| 119 | + const pj = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')); |
| 120 | + if (pj.main) { main = path.join(dir, pj.main); } |
| 121 | + } catch { /* default to extension.js */ } |
| 122 | + |
| 123 | + // Bust any cached modules under this package so sub-requires reload too. |
| 124 | + for (const k of Object.keys(require.cache)) { if (k.startsWith(dir + path.sep)) { delete require.cache[k]; } } |
| 125 | + |
| 126 | + const m = new Module(main, null); |
| 127 | + m.filename = main; |
| 128 | + m.paths = Module._nodeModulePaths(path.dirname(main)); |
| 129 | + const baseRequire = m.require.bind(m); |
| 130 | + // @ts-ignore — intercept the bare 'vscode' specifier |
| 131 | + m.require = (id) => (id === 'vscode' ? vscode : baseRequire(id)); |
| 132 | + m._compile(fs.readFileSync(main, 'utf8'), main); |
| 133 | + return m.exports; |
| 134 | +} |
| 135 | + |
| 136 | +function makeContext(dir) { |
| 137 | + const noState = { get: () => undefined, update: async () => {}, keys: () => [] }; |
| 138 | + return { |
| 139 | + subscriptions: [], |
| 140 | + extensionPath: dir, |
| 141 | + extensionUri: vscode.Uri.file(dir), |
| 142 | + globalState: noState, |
| 143 | + workspaceState: noState, |
| 144 | + extensionMode: 2 // Development |
| 145 | + }; |
| 146 | +} |
| 147 | + |
| 148 | +function teardownDev(keepStatus) { |
| 149 | + if (!dev) { return; } |
| 150 | + try { if (dev.exports && typeof dev.exports.deactivate === 'function') { dev.exports.deactivate(); } } catch (e) { output.appendLine('[dev] deactivate error: ' + e); } |
| 151 | + for (const d of dev.ctx.subscriptions) { try { if (d && d.dispose) { d.dispose(); } } catch { /* ignore */ } } |
| 152 | + dev.ctx.subscriptions = []; |
| 153 | + if (!keepStatus) { |
| 154 | + if (dev.timer) { clearTimeout(dev.timer); } |
| 155 | + try { dev.watcher.close(); } catch { /* ignore */ } |
| 156 | + dev.status.dispose(); |
| 157 | + } |
| 158 | +} |
| 159 | + |
| 160 | +function activatePackage() { |
| 161 | + const ctx = makeContext(dev.dir); |
| 162 | + dev.ctx = ctx; |
| 163 | + const exports = loadPackageExports(dev.dir); |
| 164 | + dev.exports = exports; |
| 165 | + if (typeof exports.activate === 'function') { exports.activate(ctx); } |
| 166 | + output.appendLine('[dev] activated ' + dev.name + ' (' + ctx.subscriptions.length + ' disposable(s))'); |
| 167 | +} |
| 168 | + |
| 169 | +function reloadDev() { |
| 170 | + if (!dev) { return; } |
| 171 | + teardownDev(true); |
| 172 | + try { |
| 173 | + activatePackage(); |
| 174 | + dev.status.text = '$(sync) dev: ' + dev.name; |
| 175 | + vscode.window.setStatusBarMessage('Atom++: reloaded ' + dev.name, 1500); |
| 176 | + setTimeout(() => { if (dev) { dev.status.text = '$(circle-filled) dev: ' + dev.name; } }, 600); |
| 177 | + } catch (e) { |
| 178 | + const msg = (e && e.stack) ? e.stack : String(e); |
| 179 | + output.appendLine('[dev] ERROR activating ' + dev.name + ':\n' + msg); |
| 180 | + vscode.window.showErrorMessage('Atom++: ' + dev.name + ' failed to load: ' + ((e && e.message) || e), 'Show Log') |
| 181 | + .then((c) => { if (c) { output.show(true); } }); |
| 182 | + if (dev) { dev.status.text = '$(error) dev: ' + dev.name; } |
| 183 | + } |
| 184 | +} |
| 185 | + |
| 186 | +function startDev(dir) { |
| 187 | + if (dev) { stopDev(); } |
| 188 | + const name = path.basename(dir); |
| 189 | + const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, -10); |
| 190 | + status.command = 'atompp.package.stop'; |
| 191 | + status.text = '$(circle-filled) dev: ' + name; |
| 192 | + status.tooltip = 'Atom++ is hot-reloading “' + name + '”. Click to stop.'; |
| 193 | + status.show(); |
| 194 | + |
| 195 | + const watcher = fs.watch(dir, { recursive: true }, (_event, file) => { |
| 196 | + if (file && /node_modules|\.git|\.vscode/.test(String(file))) { return; } |
| 197 | + if (file && !/\.js$/.test(String(file))) { return; } |
| 198 | + if (dev && dev.timer) { clearTimeout(dev.timer); } |
| 199 | + if (dev) { dev.timer = setTimeout(reloadDev, 150); } // debounce burst saves |
| 200 | + }); |
| 201 | + |
| 202 | + dev = { dir, name, exports: null, ctx: { subscriptions: [] }, watcher, status, timer: null }; |
| 203 | + reloadDev(); |
| 204 | + vscode.window.showInformationMessage('Atom++: developing “' + name + '” with hot reload. Save to re-run.'); |
| 205 | +} |
| 206 | + |
| 207 | +function stopDev() { |
| 208 | + if (!dev) { vscode.window.showInformationMessage('Atom++: no package is being developed.'); return; } |
| 209 | + const name = dev.name; |
| 210 | + teardownDev(false); |
| 211 | + dev = null; |
| 212 | + vscode.window.setStatusBarMessage('Atom++: stopped developing ' + name, 2000); |
| 213 | +} |
| 214 | + |
| 215 | +async function developPackage() { |
| 216 | + let dirs = []; |
| 217 | + try { dirs = fs.existsSync(PACKAGES_DIR) ? fs.readdirSync(PACKAGES_DIR).map((n) => path.join(PACKAGES_DIR, n)).filter((p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } }) : []; } catch { dirs = []; } |
| 218 | + |
| 219 | + /** @type {any[]} */ |
| 220 | + const items = dirs.map((d) => ({ label: '$(package) ' + path.basename(d), description: d, _dir: d })); |
| 221 | + items.push({ label: '$(folder-opened) Choose a folder…', _dir: null }); |
| 222 | + const pick = await vscode.window.showQuickPick(items, { placeHolder: 'Develop which package? (hot-reloads into this window)' }); |
| 223 | + if (!pick) { return; } |
| 224 | + |
| 225 | + let dir = pick._dir; |
| 226 | + if (!dir) { |
| 227 | + const picked = await vscode.window.showOpenDialog({ canSelectFolders: true, canSelectFiles: false, canSelectMany: false, openLabel: 'Develop this package', title: 'Pick an Atom++ package folder' }); |
| 228 | + if (!picked || !picked.length) { return; } |
| 229 | + dir = picked[0].fsPath; |
| 230 | + } |
| 231 | + if (!fs.existsSync(path.join(dir, 'extension.js')) && !fs.existsSync(path.join(dir, 'package.json'))) { |
| 232 | + vscode.window.showErrorMessage('Atom++: that folder has no extension.js / package.json.'); |
| 233 | + return; |
| 234 | + } |
| 235 | + startDev(dir); |
| 236 | +} |
| 237 | + |
| 238 | +function registerPackages(context) { |
| 239 | + output = vscode.window.createOutputChannel('Atom++ Packages'); |
| 240 | + context.subscriptions.push( |
| 241 | + output, |
| 242 | + vscode.commands.registerCommand('atompp.package.generate', generatePackage), |
| 243 | + vscode.commands.registerCommand('atompp.package.develop', developPackage), |
| 244 | + vscode.commands.registerCommand('atompp.package.stop', stopDev), |
| 245 | + { dispose: () => { if (dev) { teardownDev(false); dev = null; } } } |
| 246 | + ); |
| 247 | +} |
| 248 | + |
| 249 | +module.exports = { registerPackages }; |
0 commit comments