How do I bundle and read files within my extension? - vscode-extensions

I'm learning how to create vscode extensions and I'm simply trying to emulate a template plugin to be used internally within my company. I'd like to store the template in a file within the extension and then read the contents from that file and output it to the active window. So given the following example directory structure of the extension (other files omitted for brevity):
workspace/
|- templates/
| |- template1.yaml
| |- template2.yaml
|- extension.js
For the command that I'm registering, I'd simply like to read from one of those files and send it to the window as a snippet, but I don't know how to find the path to that folder
// extension.js
function activate(context) {
context.subscriptions.push(
vscode.commands.registerCommand("xxx.my-command"), function() {
let path = // ???/templates/template1.yaml
fs.readFile(path, (err, data) => {
let snippet = new vscode.SnippetString(data);
vscode.window.activeTextEditor.insertSnippet(snippet);
}
})
})
)
}
When I try to use relative paths to that templates folder, it appears to be relative to the VSCode installation. I looked around TextDocument provider etc but all of the examples were relevant to using the active editor as the document. How can I include files with my extension and access the contents of those files like this?

You should use context.asAbsolutePath, which by documentation says:
/**
* Get the absolute path of a resource contained in the extension.
*
* *Note* that an absolute uri can be constructed via {#linkcode Uri.joinPath} and
* {#linkcode ExtensionContext.extensionUri extensionUri}, e.g. `vscode.Uri.joinPath(context.extensionUri, relativePath);`
*
* #param relativePath A relative path to a resource contained in the extension.
* #return The absolute path of the resource.
*/
asAbsolutePath(relativePath: string): string;
For instance
const path = context.asAbsolutePath("templates/template1.yaml");
I also suggest you to avoid using Node's fs. Instead, use workspace.fs whenever possible. It will enable your extension to work on Remotes and Web, when necessary. More details here https://code.visualstudio.com/api/extension-guides/virtual-workspaces#review-that-the-extension-code-is-ready-for-virtual-resources
Hope this helps

I found that I could actually do a couple different things:
const path = require('path')
let templatePath = path.resolve(__dirname, './templates/template1.yaml');
But also as Mark suggested in the comments, using the extension context also provides that path:
/**
* #param {vscode.ExtensionContext} context
*/
function activate(context) {
let templatePath = context.extensionPath + '/templates/template1.yaml';
}
I went with the latter since it is being injected by the runtime which seemed better to me if not simpler. Thanks Mark!

Related

why read tsconfig.json using readConfigFile instead of directly requiring the path of tsconfig.json?

Upon investigating create-react-app's configuration, I found something interesting.
// config/modules.js
...
if (hasTsConfig) {
const ts = require(resolve.sync("typescript", {
basedir: paths.appNodeModules,
}));
config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
// Otherwise we'll check if there is jsconfig.json
// for non TS projects.
} else if (hasJsConfig) {
config = require(paths.appJsConfig);
}
...
Unlike reading jsconfig.json file using direct require(paths.appJsConfig), why is here using resolve.sync and ts.readConfigFile to read the tsconfig.json?
...
if (hasTsConfig) {
config = require(paths.appTsConfig)
// Otherwise we'll check if there is jsconfig.json
// for non TS projects.
} else if (hasJsConfig) {
config = require(paths.appJsConfig);
}
...
If I change the code like just above, the result is same. (at least the console output is same.)
There must be a reason why create-react-app using such a complicated way to read the typescript config file.
Why is that?
The ts config reader is a bit smarter than simply reading and parsing a json file. There's two differences I can think of right now:
in tsconfig files, you can use comments. JSON.parse will throw an exception because / is not an allowed character at an arbitrary position
ts config files can extend each other. Simply parsing a JSON file will ignore the extension and you'll receive a config object that doesn't represent what typescript actually uses.

Drupal 8 custom modules - Installing additional configuration from YML files with a hook_update_N

I have a custom module which installs a specific set of configurations - these are all stored in the config/install folder, which means they are installed when the module is installed.
The configuration includes a content type, paragraphs, view modes, form modes, field storages and fields attached to both the content type and the paragraphs, etc. The idea is to use this module to install a 'feature' (a blog) and use it across multiple sites, as well as provide updates and extensions when we add more stuff to this feature.
Since upon initial install, you cannot add more configuration through the config/install folder, I've been trying to find a way to import additional configuration files through an update hook, and this is one that works:
<?php
use \Symfony\Component\Yaml\Yaml;
/**
* Installs the file upload element
*/
function MODULE_NAME_update_8002() {
// Is the flaw with this the fact that the order of loading configurations now
// matters and is a little bit more difficult to deal with?
// NOTE: YES. If, for example, you comment out the installing of the
// field_storage for the field_cb_file, but try to add the field_cb_file to
// the paragraph type, the update is successful and no errors are thrown.
// This is basically me trying to re-create the drupal configuration management
// system, without the dependency checks, etc. What is the PROPER way of
// importing additional configuration from a module through an update?
// FIXME:
$configs_to_install = [
'paragraphs.paragraphs_type.cbsf_file_download',
'field.storage.paragraph.field_cb_file',
'field.field.paragraph.cbsf_file_download.field_cb_file',
'field.field.paragraph.cbsf_file_download.field_cb_heading',
'field.field.paragraph.cbsf_file_download.field_cb_icon',
'field.field.paragraph.cbsf_file_download.field_cb_text',
'core.entity_form_display.paragraph.cbsf_file_download.default',
'core.entity_view_display.paragraph.cbsf_file_download.default',
];
foreach ($configs_to_install as $config_to_install) {
$path = drupal_get_path('module', 'MODULE_NAME') . '/config/update_8002/' . $config_to_install . '.yml';
$content = file_get_contents($path);
$parsed_yml = Yaml::parse($content);
$active_storage = \Drupal::service('config.storage');
$active_storage->write($config_to_install, $parsed_yml);
}
}
however, there are flaws with this method since it means you have to order configuration files in the right order if they depend on each other, and any dependencies that are present in the config file are not checked.
Is there a way to utilise configuration management to import config properly, in this same, 'loop over the files' way? Or to point to a folder that contains all of the config files and install them?
EDIT: There are further issues with this method - even if you've ordered the files correctly in terms of dependencies, no database tables are created. The configuration is simply 'written in' as is, and no other part of Drupal seems to be made aware that new entities were created, so they cannot run any of the functions that are otherwise ran if you were to create the entities through Drupal GUI. Definitely not the recommended way of transferring more complex configuration.
I've pushed this a step further - there is a way to use the EntityTypeManager class to create / update configurations.
2 links have helped me with this largely:
https://drupal.stackexchange.com/questions/164713/how-do-i-update-the-configuration-of-a-module
pwolanins answer at the bottom provides a function that either updates the configuration if it exists, or creates the configuration outright.
https://www.metaltoad.com/blog/programmatically-importing-drupal-8-field-configurations
the code on this page gives a clearer idea of what is happening - for each configuration that you'd like to install, you run the YML file through the respective storage manager, and then create the appropriate entity configurations, which creates all of the required DB tables.
What I ended up doing was:
Utilised a slightly modified version of pwolanins code and create a generic config updater function -
function _update_or_install_config( String $prefix, String $update_id, String $module) {
$updated = [];
$created = [];
/** #var \Drupal\Core\Config\ConfigManagerInterface $config_manager */
$config_manager = \Drupal::service('config.manager');
$files = glob(drupal_get_path('module', $module) . '/config/update_' . $update_id. '/' . $prefix . '*.yml') ;
foreach ($files as $file) {
$raw = file_get_contents($file);
$value = \Drupal\Component\Serialization\Yaml::decode($raw);
if(!is_array($value)) {
throw new \RuntimeException(sprintf('Invalid YAML file %s'), $file);
}
$type = $config_manager->getEntityTypeIdByName(basename($file));
$entity_manager = $config_manager->getEntityManager();
$definition = $entity_manager->getDefinition($type);
$id_key = $definition->getKey('id');
$id = $value[$id_key];
/** #var \Drupal\Core\Config\Entity\ConfigEntityStorage $entity_storage */
$entity_storage = $entity_manager->getStorage($type);
$entity = $entity_storage->load($id);
if ($entity) {
$entity = $entity_storage->updateFromStorageRecord($entity, $value);
$entity->save();
$updated[] = $id;
}
else {
$entity = $entity_storage->createFromStorageRecord($value);
$entity->save();
$created[] = $id;
}
}
return [
'udpated' => $updated,
'created' => $created,
];
}
I placed all of my yml files in folder config/update_8002, then utilised this function to loop over the config files in a hook_update_N function:
function MODULE_NAME_update_8002() {
$configs_to_install = [
'paragraphs.paragraphs_type.cbsf_file_download',
'core.entity_form_display.paragraph.cbsf_file_download.default',
'core.entity_view_display.paragraph.cbsf_file_download.default',
'field.storage.paragraph.field_cb_file',
'field.field.paragraph.cbsf_file_download.field_cb_file',
'field.field.paragraph.cbsf_file_download.field_cb_heading',
'field.field.paragraph.cbsf_file_download.field_cb_icon',
'field.field.paragraph.cbsf_file_download.field_cb_text',
];
foreach ($configs_to_install as $config_to_install) {
_update_or_install_config('paragraphs.paragraphs_type', '8002', 'MODULE_NAME');
_update_or_install_config('field.storage.paragraph', '8002', 'MODULE_NAME');
_update_or_install_config('field.field.paragraph', '8002', 'MODULE_NAME');
_update_or_install_config('core.entity_view_display.paragraph', '8002', 'MODULE_NAME');
_update_or_install_config('core.entity_form_display.paragraph', '8002', 'MODULE_NAME');
}
}
Note that the _update_or_install_config function loops over all of the configs in the folder that match a specific entity type manager - thus you should just include the prefix in the function, and all of the YML files that import configuration of the same type will be included.

module creation in suiteCRM

I am using SuiteCRM ( Sugar CRM 6.x community edition ) & want to create a custom login page and after successful login I want to redirect based on user type
tried to create some modules but there is no clear documentation except few of useful links, below are my queries :
Can we create custom modules without using module builder, if yes then what would be steps?
Do we need to write module in /module folder or /custom/module folder or on both place?
any link is also appreciated.
You can create a custom Login Page by modfying "modules/Users/login.tpl"
Custom Modules can be created through Modulebuilder or manually.
When creating modules manually it's important to use the right names.
The easiest way is a Plural name for the folder, table and Module and a singular name for the class.
Manual steps:
You need a Folder in modules/ named like you module (i.e. CAccounts)
In this folder you need a file named like the class (i.e CAccount.php) with something like that as content:
require_once('data/SugarBean.php');
require_once('include/utils.php');
class CAccount extends SugarBean{
var $table_name = 'caccounts';
var $object_name = 'CAccount';
var $module_dir = 'CAccounts';
var $new_schema = true;
var $name;
var $created_by;
var $id;
var $deleted;
var $date_entered;
var $date_modified;
var $modified_user_id;
var $modified_by_name;
function CAccount (){
parent::SugarBean();
}
function get_summary_text(){
return $this->name;
}
function bean_implements($interface)
{
switch($interface)
{
case 'ACL':return true;
}
return false;
}
}
In this folder you need a vardefs.php file:
$dictionary['CAccount'] = array(
'table'=>'caccounts',
'audited'=>false,
'fields'=>array (
//Your fielddefs here
)
);
require_once('include/SugarObjects/VardefManager.php');
VardefManager::createVardef('CAccounts','CAccount', array('basic'));
For the language and metadata folders look at any other module.
Next is a file at "custom/Extension/application/Ext/Include/CAccounts.include.php"
$moduleList[] = 'CAccounts';
$beanList['CAccounts'] = 'CAccount';
$beanFiles['CAccount'] = 'modules/CAccounts/CAccount.php';
A language file for the module name must be in "custom/Extension/application/Ext/Language/"
$app_list_strings['moduleList']['CAccounts'] = 'Custom Accounts';
To display the Module in your tabs you need to use "rebuild and repair" and then the "Display Modules and Subpanels" option in the admin menu.
For a custom module you don't need the "custom/" folder structure. Files there will be used by sugar if provided, but often there's no need for that in a custom module.
Guides about the Module Framework can be found on the sugarcrm support site:
http://support.sugarcrm.com/02_Documentation/04_Sugar_Developer/Sugar_Developer_Guide_6.5/03_Module_Framework

phantomjs: how to make fs.open behave the same as require, for paths

require() works based on the script path. fs.open() works on the invocation (current shell) path.
Let's say i have the tree:
dirA/
dirA/dirB/
dirA/dirB/dirC/
dirA/dirB/dirC/test.js
dirA/dirB/dirC2/include_me.js
dirA/dirB/dirC2/open_me.js
and the contents for test.js:
var myMod = require('../dirC2/include_me.js');
var fs = require('fs');
fs.open('../dirC2/open_me.js')
Regardless of the current path I am when i execute phantomjs $PATH/test.js the require will succeed, and the fs.open will fail (unless $PATH is dirC)
Is there any way to make fs.open() behave as require()?
I'm thinking something like fs.open( phantomjs.getScriptPath() + '../dirC2/open_me.js' );
but my searchfoo failed to find anything to fill in for the made-up getScriptPath method there.
Edit:
posting here the workaround i'm using in case the answer is "No".
/**
* Opens a file relative to the script path.
* this solves an inconsistency between require() and fs.open()
*/
exports.getScriptPath = function( newPath ){
var script, scriptPath;
script = exports.system.args[0];
scriptPath = script.replace(/\/[^\/]+$/, '') // removes everything from
// the last "/" until the end of
// the line, non-greedy
return scriptPath + '/';
}
this is used in a utility module i have. Also it assumes you are calling a script (won't work well if you use said module in an interactive phantomjs session) and unix style paths. if you're using windows, just add \ to the regexp. then it's used like:
filehandle = exports.fs.open( exports.getScriptPath() + '../dirC2/open_me.js', 'r' );

dynamic file path in log4php

I am new to log4php.
I would like to save the log files in the format /logs/UserId/Info_ddmmyyyy.php
where the UserId is dynamic data.
(I would basically like to save one log per user.)
Is there any way to change the log file path dynamically?
This behaviour is not supported by default. But you can extend LoggerAppenderFile (or RollingFile, DailyFile whatever your preference is) to support it.
Create your own class for that and make it load to your script.
Then extend from this class:
http://svn.apache.org/repos/asf/logging/log4php/trunk/src/main/php/appenders/LoggerAppenderFile.php
class MyAppender extends LoggerAppenderFile { ... }
You'll need to overwrite the setFile() method, similar to:
public function setFile($file) {
$path = getYourFullPath();
$this->file = $path.$file;
}
After all you need to use your new Appender in you config
log4php.appender.myAppender = MyAppender
log4php.appender.myAppender.layout = LoggerLayoutSimple
log4php.appender.myAppender.file = my.log
Please note, instead of giving your full path to the log file you now need to add a plain name. The full path (including username) must be calculated with your getYourFullPath() method.
Hope that helps!
Christian