In our study of vscode source code, we are also exploring how to write a very complex and excellent application, and also exploring why vscode is so good.
Now, we will implement our own vscode based on our own learning.
First, build an empty project and start the first step.
We may not implement every step according to vscode, that is to copy, we need to implement according to their own understanding, understand the place, according to their own ideas to write, do not understand the place, mark, do not know its role, maybe in the subsequent research, you may know why.
After building the project, when we were integrating, we found that the entry file of vscode was js and loaded in a strange way.
/**
* Main startup routine
*
* @param {string | undefined} cachedDataDir
* @param {import('./vs/base/node/languagePacks').NLSConfiguration} nlsConfig
*/
function startup(cachedDataDir, nlsConfig) {
nlsConfig._languagePackSupport = true;
process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig);
process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || '';
// Load main in AMD
perf.mark('willLoadMainBundle');
require('./bootstrap-amd').load('vs/code/electron-main/main', () => {
perf.mark('didLoadMainBundle');
});
}
Copy the code
So here are the questions:
- Why its source code is TS and the entry file is JS.
- Why does loading file need to be so abnormal, it seems to use its own loader to load the page:
require('./bootstrap-amd').load('vs/code/electron-main/main', () => {
perf.mark('didLoadMainBundle');
});
Copy the code
Based on the above questions, we do not understand, first record.
I’m going to integrate TS first in my project, as I’ve written before, with a wave
- Github.com/spcBackToLi…
Next, we install electron.
yarn add electron@latest -D --registry=https://registry.npm.taobao.org/
Copy the code
Now that the basic project is set up, let’s continue with the main process entry.
Disable rendering process reuse
// Disable render process reuse, we still have
// non-context aware native modules in the renderer.
app.allowRendererProcessReuse = false;
Copy the code
This dot here is not very clear about its role in Electron. Record it first and add it later after reading it.
Start bootstrap asynchronously
Next vscode uses bootstrap, which is associated with vscode’s own asynchronous startup.
const portable = bootstrap.configurePortable(product);
Copy the code
This one, let’s ignore it, because we don’t understand the advantages of asynchronous loading, let’s record the problem and come back later.
Set up some user data
As above, record the problem and do not deal with it.
const args = parseCLIArgs();
const userDataPath = getUserDataPath(args);
app.setPath('userData', userDataPath);
// Configure static command line arguments
const argvConfig = configureCommandlineSwitchesSync(args);
Copy the code
Store the crashed directory Settings
This seems to be what the application needs, so we’ll implement it in our own application. Here we use the Minimist package to parse the parameters. Let’s do a wave.
yarn add minimist
Copy the code
Let’s write a simple test to test this feature:
// a.js
function parseCLIArgs() {
const minimist = require('minimist');
return minimist(process.argv, {
string: [
'user-data-dir'.'locale'.'js-flags'.'max-memory'.'crash-reporter-directory']}); }console.log('parseCLIArgs:', parseCLIArgs()['crash-reporter-directory']);
Copy the code
Run this script:
Those who qualify can go onto university. My-vs code Git :(Master) Qualify node A.JS --crash-reporter-directory= CCC parseCLIArgs: CCCCopy the code
ParseCLIArgs ()[‘crash-reporter-directory’] : parseCLIArgs()[‘crash-reporter-directory’]
Ok, let’s integrate in, the crash collision folder setting, which looks like an auxiliary thing, we’ll put it under utils to keep the entry file clean and tidy.
// src/utils/utils.ts
import minimist from 'minimist';
const parseCLIArgs = () = > {
return minimist(process.argv, {
string: [
'user-data-dir'.'locale'.'js-flags'.'max-memory'.'crash-reporter-directory']}); }export {
parseCLIArgs
}
Copy the code
To facilitate the management of existing variables, we create a new constant
// src/utils/constants.ts
const ARGS_NAME = {
crashReporterDirectory: 'crash-reporter-directory'
}
export {
ARGS_NAME
}
Copy the code
Continue to complete the crash directory Settings
import path from 'path';
import fs from 'fs';
import { app } from 'electron';
import { ARGS_NAME } from './constant';
const setCrashFolder = (args: any) = > {
let crashReporterDirectory = args[ARGS_NAME.crashReporterDirectory];
if (crashReporterDirectory) {
// If a variable exists, set it
crashReporterDirectory = path.normalize(crashReporterDirectory);
if(! path.isAbsolute(crashReporterDirectory)) {console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory must be absolute.`);
app.exit(1);
}
if(! fs.existsSync(crashReporterDirectory)) {try {
fs.mkdirSync(crashReporterDirectory);
} catch (error) {
console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory does not seem to exist or cannot be created.`);
app.exit(1); }}// Crashes are stored in the crashDumps directory by default, so we
// need to change that directory to the provided one
console.log(`Found --crash-reporter-directory argument. Setting crashDumps directory to be '${crashReporterDirectory}'`);
app.setPath('crashDumps', crashReporterDirectory);
} else {
// TODO}}Copy the code
If the crash variable is set at runtime, set the location of the crash variable. If the crash variable does not exist, do some things:
Here, skip content: we might have met configureCommandlineSwitchesSync
Now that we have, let’s see what works for us:
First of all, some application capability support switches, which we can set first:
const getSwitches = () = > {
const SUPPORTED_ELECTRON_SWITCHES = [
// alias from us for --disable-gpu
'disable-hardware-acceleration'.// provided by Electron
'disable-color-correct-rendering'.// override for the color profile to use
'force-color-profile'
];
if (process.platform === 'linux') {
// Force enable screen readers on Linux via this flag
SUPPORTED_ELECTRON_SWITCHES.push('force-renderer-accessibility');
}
const SUPPORTED_MAIN_PROCESS_SWITCHES = [
// Persistently enable proposed api via argv.json: https://github.com/microsoft/vscode/issues/99775
'enable-proposed-api'
];
return {
electronSwitches: SUPPORTED_ELECTRON_SWITCHES,
mainSwitches: SUPPORTED_MAIN_PROCESS_SWITCHES
}
}
const configureCommandlineSwitchesSync = (cliArgs: NativeParsedArgs) = > {
constswitches = getSwitches(); . }Copy the code
Previously, we read that adding running parameters:
node a.js --crash-reporter-directory=ccc
Copy the code
In this case, they put the run argument in: argv.json and read the configuration file in the process above, so now we need to set the path of the argv.json file.
import path from 'path';
import product from '.. /.. /production.json';
import os from 'os';
const getArgvConfigPath = () = > {
let dataFolderName = product.dataFolderName;
We add a dev suffix to // dev
if (process.env['VSCODE_DEV']) {
dataFolderName = `${dataFolderName}-dev`;
}
return path.join(os.homedir(), dataFolderName, 'argv.json');
}
export {
getArgvConfigPath
}
Copy the code
The above example involves environment variables, so let’s also define a constant to manage environment variables.
//src/utils/constant.ts
const PROCESS_ENV = {
MY_VSCODE_DEV: 'MY_VSCODE_DEV'
}
...
Copy the code
.if (process.env[PROCESS_ENV.MY_VSCODE_DEV]) {
dataFolderName = `${dataFolderName}-dev`;
}
Copy the code
In the process, he also defines a prodcwintion. json file to describe some information about the product, including dataFolderName, which is also needed here. We also create a new prodcwintion. json file
// production.json
{
"dataFolderName": ".my-vscode"
}
Copy the code
Next, we write the read variable configuration file:
const readArgvConfigSync = () = > {
// Read or create the argv.json config file sync before app('ready')
const argvConfigPath = getArgvConfigPath();
let argvConfig;
try {
argvConfig = JSON.parse(stripComments(fs.readFileSync(argvConfigPath).toString()));
} catch (error) {
console.warn(`
Unable to read argv.json configuration file in ${argvConfigPath}, falling back to defaults (${error}) `
);
}
// Fallback to default
if(! argvConfig) { argvConfig = {// Force pre-Chrome-60 color profile handling (for https://github.com/microsoft/vscode/issues/51791)
'disable-color-correct-rendering': true
};
}
return argvConfig;
}
Copy the code
- If the configuration file exists, it will be read, otherwise it will create a default configuration
So here, we have this function called stripComments, which looks like it’s doing some formatting after the file has been read, so let’s add it.
Why not read json files instead of reading them directly? Leave a question 🤔️
const stripComments = (content) = > {
const regexp = / (" (? : [^ \ \] * (?" : \ \.) ?). * ") | (' (? : [^ \ \ '] * (? : \ \.) ?). * ') | (\ / \ * (? :\r? \n|.) *? \ \ * /) | (\ / {2}. *? (? : (? :\r? \n)|$))/g;
return content.replace(regexp, function (match, m1, m2, m3, m4) {
// Only one of m1, m2, m3, m4 matches
if (m3) {
// A block comment. Replace with nothing
return ' ';
} else if (m4) {
// A line comment. If it ends in \r? \n then keep it.
const length_1 = m4.length;
if (length_1 > 2 && m4[length_1 - 1= = ='\n') {
return m4[length_1 - 2= = ='\r' ? '\r\n' : '\n';
}
else {
return ' '; }}else {
// We match a string
returnmatch; }}); }Copy the code
Now that we’ve come full circle, let’s continue configuring: CommandLine Switches.
import { readArgvConfigSync } from './argv'; .const configureCommandlineSwitchesSync = (cliArgs: NativeParsedArgs) = > {
const switches = getSwitches();
const argvConfig = readArgvConfigSync();
}
Copy the code
Next, it is up to you to decide whether to turn on these capabilities based on the switch options configured in argv.json. For clarity, let’s make a simple adjustment:
import { app } from 'electron';
const setSwitches = (supportElectronSwitches: any) = > {
const argvConfig = readArgvConfigSync();
Object.keys(argvConfig).forEach(argvKey= > {
const argvValue = argvConfig[argvKey];
// Append Electron flags to Electron
if(supportElectronSwitches.indexOf(argvKey) ! = = -1) {
// Color profile
if (argvKey === 'force-color-profile') {
if(argvValue) { app.commandLine.appendSwitch(argvKey, argvValue); }}// Others
else if (argvValue === true || argvValue === 'true') {
if (argvKey === 'disable-hardware-acceleration') {
app.disableHardwareAcceleration(); // needs to be called explicitly
} else{ app.commandLine.appendSwitch(argvKey); }}}// Append main process flags to process.argv
else if(supportElectronSwitches.indexOf(argvKey) ! = = -1) {
if (argvKey === 'enable-proposed-api') {
if (Array.isArray(argvValue)) {
argvValue.forEach(id= > id && typeof id === 'string' && process.argv.push('--enable-proposed-api', id));
} else {
console.error(`Unexpected value for \`enable-proposed-api\` in argv.json. Expected array of extension ids.`); }}}}); }const configureCommandlineSwitchesSync = (cliArgs: NativeParsedArgs) = > {
constswitches = getSwitches(); setSwitches(switches.electronSwitches); . }Copy the code
Max_old_space_size = max_old_space_size = max_old_space_size = max_old_space_size = max_old_space_size
// src/utils/commandLineSwitches.ts
const getJSFlags = (cliArgs: NativeParsedArgs) = > {
const jsFlags = [];
// Add any existing JS flags we already got from the command line
if (cliArgs['js-flags']) {
jsFlags.push(cliArgs['js-flags']);
}
// Support max-memory flag
if (cliArgs['max-memory'] &&!/max_old_space_size=(\d+)/g.exec(cliArgs['js-flags'])) {
jsFlags.push(`--max_old_space_size=${cliArgs['max-memory']}`);
}
return jsFlags.length > 0 ? jsFlags.join(' ') : null;
}
const configureCommandlineSwitchesSync = (cliArgs: NativeParsedArgs) = > {
const switches = getSwitches();
setSwitches(switches.electronSwitches);
// Support JS Flags
const jsFlags = getJSFlags(cliArgs);
if (jsFlags) {
app.commandLine.appendSwitch('js-flags', jsFlags);
}
return argvConfig;
}
Copy the code
To this, we’re done with configureCommandlineSwitchesSync, alas! I didn’t expect that there would be so many things to consider in order to make a Electron. I haven’t started to write the content yet, and I’m still making preparations.
Remember the configureCommandlineSwitchesSync in before, we are setting crash directory, we continue to work,
However, it seems that the following logic is based on the existence of appCenter, I see vscode is not configured, we do not use it, forget it, remember TODO.
const appCenter = product.appCenter;
// Disable Appcenter crash reporting if
// * --crash-reporter-directory is specified
// * enable-crash-reporter runtime argument is set to 'false'
// * --disable-crash-reporter command line parameter is set
if (appCenter && argvConfig['enable-crash-reporter'] && !args['disable-crash-reporter']) {... }Copy the code
Ok, set up the logic for conflicting folders and we’re done!
import { configureCommandlineSwitchesSync } from './utils/commandLineSwitches';
import { setCrashFolder } from './utils/crash';
import { parseCLIArgs } from './utils/utils';
const args = parseCLIArgs();
// Argv parameter configuration
const argvConfig = configureCommandlineSwitchesSync(args);
setCrashFolder(args, argvConfig);
Copy the code
Next, it looks like the crashRepoter information has been set up. This is worth learning and using. Let’s add some product information in production.json:
{
"nameShort": "my-vscode"."companyName": "pk"
}
Copy the code
We need to enable json support to read production.json.
{
"compilerOptions": {
"strictNullChecks": true."module": "commonjs"."esModuleInterop": true."importHelpers": true."allowSyntheticDefaultImports": true."target": "es5"."resolveJsonModule": true."lib": [
"es2015"]},"files": [
"**/*.json"]."include": [
"src" // write ts in src, you can define your own dir]}Copy the code
Here, we run into the crashReporter path setting problem, which is related to appCenter. We want this configuration, so let’s add this function.
// production.json
{
"dataFolderName": ".my-vscode"."nameShort": "my-vscode"."companyName": "pk"."appCenter": {
"win32-ia32": ""."win32-x64": ""."linux-x64": ""."drawn": ""}}Copy the code
We matched it first, then left it empty, and added the corresponding Crash alarm URL for subsequent use.
So, we add this logic to the setting Crash folder, and finally the main process logic:
// src/main.ts
import { configureCommandlineSwitchesSync } from './utils/commandLineSwitches';
import { startCrash } from './utils/crash';
import { parseCLIArgs } from './utils/utils';
const args = parseCLIArgs();
// Argv parameter configuration
const argvConfig = configureCommandlineSwitchesSync(args);
/ / set the crasher
startCrash(args, argvConfig);
Copy the code
// src/utils/crash.ts
import path from 'path';
import fs from 'fs';
import { app, crashReporter } from 'electron';
import product from '.. /.. /production.json';
import { ARGS_NAME } from './constant';
const setCrashFolder = (args: any, argvConfig: any): {
submitURL: string;
crashReporterDirectory: string
} => {
let submitURL = ' ';
let crashReporterDirectory = args[ARGS_NAME.crashReporterDirectory];
if (crashReporterDirectory) {
// If a variable exists, set it
crashReporterDirectory = path.normalize(crashReporterDirectory);
if(! path.isAbsolute(crashReporterDirectory)) {console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory must be absolute.`);
app.exit(1);
}
if(! fs.existsSync(crashReporterDirectory)) {try {
fs.mkdirSync(crashReporterDirectory);
} catch (error) {
console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory does not seem to exist or cannot be created.`);
app.exit(1); }}// Crashes are stored in the crashDumps directory by default, so we
// need to change that directory to the provided one
console.log(`Found --crash-reporter-directory argument. Setting crashDumps directory to be '${crashReporterDirectory}'`);
app.setPath('crashDumps', crashReporterDirectory);
} else {
const appCenter = product.appCenter;
// Disable Appcenter crash reporting if
// * --crash-reporter-directory is specified
// * enable-crash-reporter runtime argument is set to 'false'
// * --disable-crash-reporter command line parameter is set
if(appCenter && argvConfig[ARGS_NAME.enableCrashReporter] && ! args[ARGS_NAME.disableCrashReporter]) {const isWindows = (process.platform === 'win32');
const isLinux = (process.platform === 'linux');
const crashReporterId = argvConfig[ARGS_NAME.crashReporterId];
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (uuidPattern.test(crashReporterId)) {
submitURL = isWindows ? appCenter[process.arch === 'ia32' ? 'win32-ia32' : 'win32-x64'] : isLinux ? appCenter[`linux-x64`] : appCenter.darwin;
submitURL = submitURL.concat('&uid=', crashReporterId, '&iid=', crashReporterId, '&sid=', crashReporterId);
// Send the id for child node process that are explicitly starting crash reporter.
// For vscode this is ExtensionHost process currently.
const argv = process.argv;
const endOfArgsMarkerIndex = argv.indexOf(The '-');
if (endOfArgsMarkerIndex === -1) {
argv.push('--crash-reporter-id', crashReporterId);
} else {
// if the we have an argument "--" (end of argument marker)
// we cannot add arguments at the end. rather, we add
// arguments before the "--" marker.
argv.splice(endOfArgsMarkerIndex, 0.'--crash-reporter-id', crashReporterId); }}}}return {
submitURL,
crashReporterDirectory
}
}
const startCrash = (args: any, argvConfig: any) = > {
const productName = product.nameShort;
const companyName = product.companyName;
const result = setCrashFolder(args, argvConfig);
crashReporter.start({
companyName: companyName,
productName: process.env['VSCODE_DEV']?`${productName} Dev` : productName,
submitURL: result.submitURL,
uploadToServer: !result.crashReporterDirectory
});
}
export {
startCrash
}
Copy the code
Today first here, tomorrow continue to refueling!!