Merge pull request #13657 from keymanapp/chore/merge-dev-refactors-into-web-core

chore: merge dev refactors into epic/web-core 🎼
This commit is contained in:
Marc Durdin 2025-04-03 21:45:34 +07:00 • committed by GitHub
commit 5c5b44df02
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 625 additions and 203 deletions

View file

@ -1,5 +1,13 @@
# Keyman Version History
## 19.0.20 alpha 2025-04-01
* fix(android): move responsibility for setting keyboard height preference into `applyKeyboardHeight()` (#13578)
## 19.0.19 alpha 2025-03-31
* chore: merge beta to master B18S4 (#13606)
## 19.0.18 alpha 2025-03-25
* chore(developer): improve ldml code coverage (#13516)

View file

@ -1 +1 @@
19.0.19
19.0.21

View file

@ -4,7 +4,6 @@
package com.tavultesoft.kmapro;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
@ -34,7 +33,6 @@ public class AdjustKeyboardHeightActivity extends BaseActivity {
private static ImageView sampleKeyboard = null;
// Keeps track of the adjusted keyboard height for saving
private static SharedPreferences.Editor editor = null;
private static int currentHeight = 0;
private static ViewGroup.LayoutParams layoutParams;
@ -65,9 +63,6 @@ public class AdjustKeyboardHeightActivity extends BaseActivity {
adjustKeyboardHeightActivityTitle.setTextColor(ContextCompat.getColor(this, R.color.ms_white));
adjustKeyboardHeightActivityTitle.setText(titleStr);
SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
editor = prefs.edit();
sampleKeyboard = (ImageView) findViewById(R.id.sample_keyboard);
layoutParams = sampleKeyboard.getLayoutParams();
currentHeight = KMManager.getKeyboardHeight(context);
@ -77,9 +72,7 @@ public class AdjustKeyboardHeightActivity extends BaseActivity {
resetButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// Clear the keyboard height preferences to reset
editor.remove(KMManager.KMKey_KeyboardHeightPortrait);
editor.remove(KMManager.KMKey_KeyboardHeightLandscape);
editor.commit();
KMManager.applyKeyboardHeight(context, KMManager.KeyboardHeight_Reset);
// Restore default height
currentHeight = KMManager.getKeyboardHeight(context);
@ -107,11 +100,7 @@ public class AdjustKeyboardHeightActivity extends BaseActivity {
break;
case MotionEvent.ACTION_UP:
// Save the currentHeight when the user releases
int orientation = KMManager.getOrientation(context);
String keyboardHeightKey = (orientation == Configuration.ORIENTATION_LANDSCAPE) ?
KMManager.KMKey_KeyboardHeightLandscape : KMManager.KMKey_KeyboardHeightPortrait;
editor.putInt(keyboardHeightKey, currentHeight);
editor.commit();
KMManager.applyKeyboardHeight(context, currentHeight);
break;
}
return true;
@ -139,12 +128,5 @@ public class AdjustKeyboardHeightActivity extends BaseActivity {
refreshSampleKeyboard(this);
}
@Override
public void onBackPressed() {
// Apply the adjusted height on exit
KMManager.applyKeyboardHeight(this, currentHeight);
super.onBackPressed();
}
}

View file

@ -25,6 +25,7 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Typeface;
import android.inputmethodservice.InputMethodService;
@ -273,6 +274,8 @@ public final class KMManager {
// will be blocked.
private static boolean maySendCrashReport = true;
private final static String KMEngine_PrefsKey = "KMAPreferences";
// Keyman public keys
public static final String KMKey_ID = "id";
public static final String KMKey_Name = "name";
@ -295,8 +298,8 @@ public final class KMManager {
public static final String KMKey_KMPInstall_Mode = "kmpInstallMode";
public static final String KMKey_KeyboardModified = "lastModified";
public static final String KMKey_KeyboardRTL = "rtl";
public static final String KMKey_KeyboardHeightPortrait = "keyboardHeightPortrait";
public static final String KMKey_KeyboardHeightLandscape = "keyboardHeightLandscape";
private static final String KMKey_KeyboardHeightPortrait = "keyboardHeightPortrait";
private static final String KMKey_KeyboardHeightLandscape = "keyboardHeightLandscape";
public static final String KMKey_LongpressDelay = "longpressDelay";
@ -345,6 +348,14 @@ public final class KMManager {
public static final int KMMinimum_LongpressDelay = 300;
public static final int KMMaximum_LongpressDelay = 1500;
// Default keyboard heights
public static final int KeyboardHeight_Reset = 0; // To reset to default, applyKeyboardHeight
public static final int KeyboardHeight_Invalid = -1; // If invalid orientation passed to functions
public static int KeyboardHeight_Context_Portrait_Default = 0; // Default portrait height
public static int KeyboardHeight_Context_Landscape_Default = 0; // Default landscape height
public static int KeyboardHeight_Context_Portrait_Current = 0; // Current portrait height
public static int KeyboardHeight_Context_Landscape_Current = 0; // Current landscape height
// Default prediction/correction setting
public static final int KMDefault_Suggestion = SuggestionType.PREDICTIONS_WITH_CORRECTIONS.toInt();
@ -498,6 +509,12 @@ public final class KMManager {
migrateCloudKeyboards(appContext);
CloudDownloadMgr.getInstance().initialize(appContext);
calculateDefaultKeyboardHeights(context);
SharedPreferences prefs = context.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE);
KeyboardHeight_Context_Portrait_Current = prefs.getInt(KMManager.KMKey_KeyboardHeightPortrait, KMManager.KeyboardHeight_Context_Portrait_Default);
KeyboardHeight_Context_Landscape_Current = prefs.getInt(KMManager.KMKey_KeyboardHeightLandscape, KMManager.KeyboardHeight_Context_Landscape_Default);
}
public static void executeResourceUpdate(Context aContext)
@ -1463,7 +1480,7 @@ public final class KMManager {
* @param suggestType SuggestionType
*/
public static void setMaySuggest(String languageID, SuggestionType suggestType) {
SharedPreferences prefs = appContext.getSharedPreferences(appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
SharedPreferences prefs = appContext.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(KMManager.getLanguageAutoCorrectionPreferenceKey(languageID), suggestType.toInt());
editor.commit();
@ -1582,7 +1599,7 @@ public final class KMManager {
model = model.replaceAll("\"", "'");
// When entering password field, maySuggest should override to disabled
SharedPreferences prefs = appContext.getSharedPreferences(appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
SharedPreferences prefs = appContext.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE);
int suggestionPreference = prefs.getInt(getLanguageAutoCorrectionPreferenceKey(languageID), KMDefault_Suggestion);
RelativeLayout.LayoutParams params;
@ -2213,7 +2230,7 @@ public final class KMManager {
*/
public static int getLongpressDelay() {
SharedPreferences prefs = appContext.getSharedPreferences(
appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE);
return prefs.getInt(KMKey_LongpressDelay, KMDefault_LongpressDelay);
}
@ -2230,7 +2247,7 @@ public final class KMManager {
}
SharedPreferences prefs = appContext.getSharedPreferences(
appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(KMKey_LongpressDelay, longpressDelay);
editor.commit();
@ -2266,21 +2283,80 @@ public final class KMManager {
return bannerHeight;
}
/**
* Returns the current keyboard height in dp for the current device orientation
*/
public static int getKeyboardHeight(Context context) {
int defaultHeight = (int) context.getResources().getDimension(R.dimen.keyboard_height);
SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
int orientation = getOrientation(context);
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
return prefs.getInt(KMManager.KMKey_KeyboardHeightPortrait, defaultHeight);
} else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
return prefs.getInt(KMManager.KMKey_KeyboardHeightLandscape, defaultHeight);
}
return defaultHeight;
return getKeyboardHeight(context, orientation);
}
/**
* Returns the current keyboard height in dp for the specified device
* orientation
*/
public static int getKeyboardHeight(Context context, int orientation) {
SharedPreferences prefs = context.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE);
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
return KMManager.KeyboardHeight_Context_Portrait_Current;
} else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
return KMManager.KeyboardHeight_Context_Landscape_Current;
} else {
return KMManager.KeyboardHeight_Invalid; // Invalid orientation
}
}
/**
* Apply the keyboard height for the current orientation and store it in SharedPreferences.
* If height is KMManager.KeyboardHeight_Reset, remove the stored height for that
* orientation, resetting to the default height for the device/orientation.
* @param context Context
* @param height int - keyboard height in dp
*/
public static void applyKeyboardHeight(Context context, int height) {
int orientation = getOrientation(context);
applyKeyboardHeight(context, height, orientation);
}
/**
* Apply the keyboard height for the specified orientation and store it in SharedPreferences.
* If height is KMManager.KeyboardHeight_Reset, remove the stored height for that orientation.
* @param context Context
* @param height int - keyboard height in dp
* @param orientation int - Configuration.ORIENTATION_PORTRAIT or Configuration.ORIENTATION_LANDSCAPE
*/
public static void applyKeyboardHeight(Context context, int height, int orientation) {
SharedPreferences prefs = context.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
int defaultHeightForContext = getDefaultKeyboardHeight(orientation);
if (orientation != Configuration.ORIENTATION_PORTRAIT && orientation != Configuration.ORIENTATION_LANDSCAPE) {
return; // Invalid orientation, do nothing
}
if (height == KMManager.KeyboardHeight_Reset) {
// Passing 0 will reset the stored height for this orientation.
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
editor.remove(KMManager.KMKey_KeyboardHeightLandscape);
} else /* (orientation == Configuration.ORIENTATION_PORTRAIT) */{
editor.remove(KMManager.KMKey_KeyboardHeightPortrait);
}
height = defaultHeightForContext;
} else {
// Applying gating to 50%-200% of default height (following Keyman)
if (height < (defaultHeightForContext / 2)) {
height = (int) (defaultHeightForContext / 2);
} else if (height > (defaultHeightForContext * 2)) {
height = (int) (defaultHeightForContext * 2);
}
// Store the new height based on the current orientation
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
editor.putInt(KMManager.KMKey_KeyboardHeightLandscape, height);
} else /* (orientation == Configuration.ORIENTATION_PORTRAIT) */ {
editor.putInt(KMManager.KMKey_KeyboardHeightPortrait, height);
}
}
editor.commit();
// Confirm new LayoutParams for in-app or system keyboards
if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP)) {
InAppKeyboard.loadJavascript(KMString.format("setOskHeight('%s')", height));
RelativeLayout.LayoutParams params = getKeyboardLayoutParams();
@ -2293,6 +2369,53 @@ public final class KMManager {
}
}
/**
* Returns the default keyboard height in dp for the current device
* and orientation
*/
public static int getDefaultKeyboardHeight(Context context) {
int orientation = getOrientation(context);
return getDefaultKeyboardHeight(orientation);
}
/**
* Returns the current keyboard height in dp for the specified orientation
* for the current device
*/
public static int getDefaultKeyboardHeight(int orientation) {
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
return KMManager.KeyboardHeight_Context_Portrait_Default;
} else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
return KMManager.KeyboardHeight_Context_Landscape_Default;
} else {
return KMManager.KeyboardHeight_Invalid; // Invalid orientation
}
}
private static void calculateDefaultKeyboardHeights(Context context) {
if (isTestMode()) {
// Keyboard height not needed for unit tests #13578
KeyboardHeight_Context_Portrait_Default = 0;
KeyboardHeight_Context_Landscape_Default = 0;
return;
}
Resources resources = context.getResources();
Configuration originalConfig = resources.getConfiguration();
Configuration newConfig = new Configuration(originalConfig);
newConfig.orientation = Configuration.ORIENTATION_PORTRAIT;
Context newContext = context.createConfigurationContext(newConfig);
Resources newResources = newContext.getResources();
KeyboardHeight_Context_Portrait_Default = (int) newResources.getDimension(R.dimen.keyboard_height);
newConfig = new Configuration(originalConfig);
newConfig.orientation = Configuration.ORIENTATION_LANDSCAPE;
newContext = context.createConfigurationContext(newConfig);
newResources = newContext.getResources();
KeyboardHeight_Context_Landscape_Default = (int) newResources.getDimension(R.dimen.keyboard_height);
}
/**
* Get the size of the area the window would occupy.
* API 30+
@ -2322,7 +2445,7 @@ public final class KMManager {
}
protected static void setPersistentShouldShowHelpBubble(boolean flag) {
SharedPreferences prefs = appContext.getSharedPreferences(appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
SharedPreferences prefs = appContext.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(KMManager.KMKey_ShouldShowHelpBubble, flag);
editor.commit();

View file

@ -5,12 +5,17 @@ title: KMManager.applyKeyboardHeight()
## Summary
The **applyKeyboardHeight()** method sets the height of the keyboard frame for
the device's current [screen orientation](https://developer.android.com/training/multiscreen/screensizes#TaskUseOriQuali)
(portrait vs landscape).
(portrait vs landscape) or a specified orientation.
## Syntax
```java
KMManager.applyKeyboardHeight(Context context, int height)
```
or
```java
KMManager.applyKeyboardHeight(Context context, int height, int orientation)
```
### Parameters
@ -18,7 +23,10 @@ KMManager.applyKeyboardHeight(Context context, int height)
: The context
`height`
: The height of the keyboard frame in *density-independent pixels (dp)*
: The height of the keyboard frame in *density-independent pixels (dp)*. Pass `KMManager.KeyboardHeight_Reset` to reset the keyboard height (for current orientation) to device-specific defaults.
`orientation` _(Optional)_
: Accepts a [screen orientation](https://developer.android.com/training/multiscreen/screensizes#TaskUseOriQuali) value. This is most useful if you want to change the size of the keyboard in the other (non-current) orientation. If `orientation` is not defined, keyboard height is set for the current device orientation.
## Description
Use this method when you want to increase or decrease the keyboard height for
@ -30,11 +38,11 @@ For reference, here's a table of the default Keyman keyboard heights for various
Device Type and Screen Orientation | Default height (dp) |
|-----------------------------------|---------------------|
| Default handset in portrait | 205 |
| Default handset in landscape | 150 |
| Default handset in landscape | 100 |
| 7" tablet in portrait | 305 |
| 7" tablet in landscape | 200 |
| 7" tablet in landscape | 140 |
| 10" tablet in portrait | 405 |
| 10" tablet in landscape | 300 |
| 10" tablet in landscape | 200 |
**Note:** This new keyboard height would be applied for all platforms, so an
adjusted keyboard height for a phone would appear too small for a tablet.
@ -50,5 +58,23 @@ The following script illustrates the use of `applyKeyboardHeight()`:
KMManager.applyKeyboardHeight(this, newKeyboardHeight);
```
or
```java
import android.content.res.Configuration;
...
// Increase the Keyman keyboard height for landscape mode to 250dp (default Keyman value for most phones is 100dp)
int newKeyboardHeight = 250;
KMManager.applyKeyboardHeight(this, newKeyboardHeight, Configuration.ORIENTATION_LANDSCAPE);
```
The following script illustrates the use of `applyKeyboardHeight()` to reset keyboard height to default:
```java
// Reset keyboard to default height
KMManager.applyKeyboardHeight(this, KMManager.KeyboardHeight_Reset);
```
## See also
* [getKeyboardHeight()](getKeyboardHeight)

View file

@ -0,0 +1,55 @@
---
title: KMManager.getDefaultKeyboardHeight()
---
## Summary
The **`getDefaultKeyboardHeight()`** method returns the default height of the keyboard frame for the current or specefied orientation.
## Syntax
``` javascript
KMManager.getDefaultKeyboardHeight(Context context)
```
or
``` javascript
KMManager.getDefaultKeyboardHeight(int orientation)
```
### Parameters (Choose One)
`context` _(Alternate)_
: The context.
`orientation` _(Alternate)_
: Accepts a [screen orientation](https://developer.android.com/training/multiscreen/screensizes#TaskUseOriQuali) value. This is most useful if you want to get the size of the keyboard in the other (non-current) orientation. If `orientation` parameter is not passed in, keyboard height is returned for the current device orientation.
### Returns
Returns the default height of the keyboard frame in *density-independent pixels (dp)*.
## Description
Use this method to get the default height of the keyboard frame.
## Examples
### Example: Using `getDefaultKeyboardHeight()`
The following script illustrate the use of `getDefaultKeyboardHeight()`:
``` javascript
int keyboardHeight = KMManager.getDefaultKeyboardHeight(this);
```
or
```java
import android.content.res.Configuration;
...
// Get the current Keyman keyboard height for landscape mode.
int keyboardHeightLandscape = KMManager.getDefaultKeyboardHeight( Configuration.ORIENTATION_LANDSCAPE);
```
## See also
- [applyKeyboardHeight()](applyKeyboardHeight)

View file

@ -12,12 +12,19 @@ frame.
``` javascript
KMManager.getKeyboardHeight(Context context)
```
or
``` javascript
KMManager.getKeyboardHeight(Context context, int orientation)
```
### Parameters
`context`
: The context.
`orientation` _(Optional)_
: Accepts a [screen orientation](https://developer.android.com/training/multiscreen/screensizes#TaskUseOriQuali) value. This is most useful if you want to get the size of the keyboard in the other (non-current) orientation. If `orientation` parameter is not passed in, keyboard height is returned for the current device orientation.
### Returns
Returns the height of the keyboard frame in *density-independent pixels
@ -36,7 +43,15 @@ The following script illustrate the use of `getKeyboardHeight()`:
``` javascript
int keyboardHeight = KMManager.getKeyboardHeight(this);
```
or
```java
import android.content.res.Configuration;
...
// Get the current Keyman keyboard height for landscape mode.
int keyboardHeightLandscape = KMManager.getKeyboardHeight(this, Configuration.ORIENTATION_LANDSCAPE);
```
## See also
- [applyKeyboardHeight](applyKeyboardHeight)
- [applyKeyboardHeight()](applyKeyboardHeight)

View file

@ -6,11 +6,11 @@ title: Adjust Keyboard Height - Keyman for Android Help
This menu is used to adjust the height of the on-screen keyboard (OSK), which does not include the suggestion banner.
Drag the blank OSK at the bottom of the screen to increase or decrease the keyboard height for the device's current orientation (portrait vs landscape). The adjusted keyboard heights are automatically saved when the blank OSK is released.
Drag the blank OSK at the bottom of the screen to increase or decrease the keyboard height for the device's current orientation (portrait vs landscape). Turn the device 90 degrees to the alternate orientation and repeat the process. The adjusted keyboard heights are automatically saved when the blank OSK is released.
![](../../android_images/blank-osk.png)
### Reset to Default Height
Press the button to reset the OSK height to the default values.
Press the button to reset the OSK height (for the current orientation) to the default device-specific height.
![](../../android_images/reset-to-default.png)

View file

@ -1,5 +1,5 @@
import * as fs from 'fs';
import * as rd from 'readline';
import * as fs from 'node:fs';
import * as rd from 'node:readline';
export default async function hextobin(inputFilename: string, outputFilename?: string, options?: {silent?: boolean}): Promise<Uint8Array> {

View file

@ -1,5 +1,5 @@
import * as fs from 'fs';
import path from 'path';
import * as fs from 'node:fs';
import path from 'node:path';
import convertSourceMap from 'convert-source-map'; // Transforms sourcemaps among various common formats.
// Base64, stringified-JSON, end-of-file comment...

View file

@ -1,5 +1,5 @@
import * as fs from 'fs';
import * as path from 'path';
import * as fs from 'node:fs';
import * as path from 'node:path';
// TODO: support multiple infiles to generate from multiple headers

View file

@ -1,5 +1,5 @@
import * as fs from 'fs';
import * as path from 'path';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { CompilerEvent, CompilerCallbacks, CompilerPathCallbacks, CompilerFileSystemCallbacks,
CompilerError, CompilerNetAsyncCallbacks, DefaultCompilerFileSystemAsyncCallbacks,
CompilerFileSystemAsyncCallbacks,

View file

@ -10,7 +10,6 @@
],
"dependencies": {
"@keymanapp/common-types": "*",
"@sentry/node": "^7.57.0",
"eventemitter3": "^5.0.0",
"fast-xml-parser": "^5.0.9",
"path-browserify": "^1.0.1",

View file

@ -233,9 +233,10 @@ export enum CompilerErrorNamespace {
*/
LdmlKeyboardCompiler = 0x0000,
/**
* common/web/types errors between 0x1000…0x1FFF
* common/web/utils errors between 0x1000…0x1FFF
* was previously called CommonTypes
*/
CommonTypes = 0x1000,
DeveloperUtils = 0x1000,
/**
* kmc-kmn errors between 0x2000…0x2FFF; these map to
* the base codes found in kmn_compiler_errors.h, exclusive severity flags

View file

@ -4,14 +4,14 @@
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageDef as def, CompilerMessageSpec as m } from './compiler-interfaces.js';
import { constants } from '@keymanapp/ldml-keyboard-constants';
const CommonTypesErrMask = CompilerErrorNamespace.CommonTypes;
// const SevInfo = CompilerErrorSeverity.Info | CommonTypesErrMask;
// const SevHint = CompilerErrorSeverity.Hint | CommonTypesErrMask;
// const SevWarn = CompilerErrorSeverity.Warn | CommonTypesErrMask;
const SevError = CompilerErrorSeverity.Error | CommonTypesErrMask;
// const SevFatal = CompilerErrorSeverity.Fatal | CommonTypesErrMask;
const DeveloperUtilsErrMask = CompilerErrorNamespace.DeveloperUtils;
// const SevInfo = CompilerErrorSeverity.Info | DeveloperUtilsErrMask;
// const SevHint = CompilerErrorSeverity.Hint | DeveloperUtilsErrMask;
// const SevWarn = CompilerErrorSeverity.Warn | DeveloperUtilsErrMask;
const SevError = CompilerErrorSeverity.Error | DeveloperUtilsErrMask;
// const SevFatal = CompilerErrorSeverity.Fatal | DeveloperUtilsErrMask;
export class CommonTypesMessages {
export class DeveloperUtilsMessages {
// structured Ajv validation error
static ERROR_SchemaValidationError = SevError | 0x0001;
static Error_SchemaValidationError = (o:{instancePath:string, keyword:string, message: string, params: string}) => m(this.ERROR_SchemaValidationError,
@ -58,4 +58,22 @@ export class CommonTypesMessages {
this.ERROR_InvalidPackageFile,
`Package source file is invalid: ${(o.e ?? 'unknown error').toString()}`
);
static ERROR_InvalidProjectFile = SevError | 0x000A;
static Error_InvalidProjectFile = (o:{message:string}) => m(
this.ERROR_InvalidProjectFile,
`Project file is not valid: ${def(o.message)}`,
);
static ERROR_UnsupportedProjectVersion = SevError | 0x0013;
static Error_UnsupportedProjectVersion = (o:{version:string}) => m(
this.ERROR_UnsupportedProjectVersion,
`Project version ${def(o.version)} is not supported by this version of Keyman Developer.`,
);
static ERROR_ProjectFileCouldNotBeRead = SevError | 0x0014;
static Error_ProjectFileCouldNotBeRead = () => m(
this.ERROR_ProjectFileCouldNotBeRead,
`Project file could not be read`
);
};

View file

@ -3,8 +3,6 @@
*/
export { validateMITLicense } from './utils/validate-mit-license.js';
export { KeymanSentry, SentryNodeOptions } from './utils/KeymanSentry.js';
export { getOption, loadOptions, clearOptions } from './utils/options.js';
export { escapeMarkdownChar } from './utils/markdown.js';
export { KeymanUrls } from './utils/keyman-urls.js';
@ -63,7 +61,7 @@ export { defaultCompilerOptions, CompilerBaseOptions, CompilerOptions, CompilerE
export { UrlSubpathCompilerCallback } from './utils/UrlSubpathCompilerCallback.js';
export { CommonTypesMessages } from './common-messages.js';
export { DeveloperUtilsMessages } from './developer-utils-messages.js';
export * as SourceFilenamePatterns from './source-filename-patterns.js';
export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader } from './xml-utils.js';
@ -73,3 +71,5 @@ export * as CloudUrls from './cloud-urls.js';
export { getFontFamily, getFontFamilySync } from './font-family.js';
export * as ValidIds from './valid-ids.js';
export * as ProjectLoader from './project-loader.js';

View file

@ -0,0 +1,34 @@
import { KeymanDeveloperProject } from "./types/kpj/keyman-developer-project.js";
import { KPJFileReader } from "./types/kpj/kpj-file-reader.js";
import { CompilerAsyncCallbacks, CompilerCallbacks } from "./compiler-callbacks.js";
import { DeveloperUtilsMessages } from "./developer-utils-messages.js";
// TODO-KMC-TEST: refactor asyncCallbacks, make it a first-class part of CompilerCallbacks
export async function loadProjectFromFile(infile: string, callbacks: CompilerCallbacks, asyncCallbacks: CompilerAsyncCallbacks = null): Promise<KeymanDeveloperProject> {
const kpjData = await (asyncCallbacks ?? callbacks).fsAsync.readFile(infile);
if(!kpjData) {
callbacks.reportMessage(DeveloperUtilsMessages.Error_ProjectFileCouldNotBeRead());
return null;
}
const reader = new KPJFileReader(callbacks);
let kpj = null;
try {
kpj = reader.read(kpjData);
if(!kpj) {
callbacks.reportMessage(DeveloperUtilsMessages.Error_ProjectFileCouldNotBeRead());
return null;
}
if(kpj.KeymanDeveloperProject?.Options?.Version && kpj.KeymanDeveloperProject.Options.Version != "1.0" && kpj.KeymanDeveloperProject.Options.Version != "2.0") {
callbacks.reportMessage(DeveloperUtilsMessages.Error_UnsupportedProjectVersion({version: kpj.KeymanDeveloperProject.Options.Version}));
return null;
}
reader.validate(kpj);
} catch(e) {
callbacks.reportMessage(DeveloperUtilsMessages.Error_InvalidProjectFile({message: (e??'').toString()}));
return null;
}
const project = await reader.transform(infile, kpj);
return project;
}

View file

@ -9,7 +9,7 @@
import { util } from '@keymanapp/common-types';
import boxXmlArray = util.boxXmlArray;
import { CommonTypesMessages } from "../../common-messages.js";
import { DeveloperUtilsMessages } from "../../developer-utils-messages.js";
import { CompilerCallbacks } from "../../compiler-callbacks.js";
import { KeymanXMLReader } from "../../xml-utils.js";
import { KpsPackage } from "./kps-file.js";
@ -29,7 +29,7 @@ export class KpsFileReader {
a = new KeymanXMLReader('kps')
.parse(data) as KpsPackage;
} catch(e) {
this.callbacks.reportMessage(CommonTypesMessages.Error_InvalidPackageFile({e}));
this.callbacks.reportMessage(DeveloperUtilsMessages.Error_InvalidPackageFile({e}));
}
return a;
})();

View file

@ -4,7 +4,7 @@
* Reads a LDML XML keyboard file into JS object tree and resolves imports
*/
import { SchemaValidators, util } from '@keymanapp/common-types';
import { CommonTypesMessages } from '../../common-messages.js';
import { DeveloperUtilsMessages } from '../../developer-utils-messages.js';
import { CompilerCallbacks } from "../../compiler-callbacks.js";
import { LDMLKeyboardXMLSourceFile, LKImport, ImportStatus } from './ldml-keyboard-xml.js';
import { constants } from '@keymanapp/ldml-keyboard-constants';
@ -219,7 +219,7 @@ export class LDMLKeyboardXMLSourceFileReader {
const { base, path } = asImport;
// If base is not an empty string (or null/undefined), then it must be 'cldr'
if (base && base !== constants.cldr_import_base) {
this.callbacks.reportMessage(CommonTypesMessages.Error_ImportInvalidBase({base, path, subtag}));
this.callbacks.reportMessage(DeveloperUtilsMessages.Error_ImportInvalidBase({base, path, subtag}));
return false;
}
let importData: Uint8Array;
@ -228,7 +228,7 @@ export class LDMLKeyboardXMLSourceFileReader {
// CLDR import
const paths = path.split('/');
if (paths[0] == '' || paths[1] == '' || paths.length !== 2) {
this.callbacks.reportMessage(CommonTypesMessages.Error_ImportInvalidPath({base, path, subtag}));
this.callbacks.reportMessage(DeveloperUtilsMessages.Error_ImportInvalidPath({base, path, subtag}));
return false;
}
if (constants.treatAsLatest(paths[0])) {
@ -241,7 +241,7 @@ export class LDMLKeyboardXMLSourceFileReader {
importData = this.readLocalImportFile(path);
}
if (!importData || !importData.length) {
this.callbacks.reportMessage(CommonTypesMessages.Error_ImportReadFail({base, path, subtag}));
this.callbacks.reportMessage(DeveloperUtilsMessages.Error_ImportReadFail({base, path, subtag}));
return false;
}
const importXml: any = this.loadUnboxed(importData); // TODO-LDML: have to load as any because it is an arbitrary part
@ -249,7 +249,7 @@ export class LDMLKeyboardXMLSourceFileReader {
// importXml will have one property: the root element.
if (!importRootNode) {
this.callbacks.reportMessage(CommonTypesMessages.Error_ImportWrongRoot({base, path, subtag}));
this.callbacks.reportMessage(DeveloperUtilsMessages.Error_ImportWrongRoot({base, path, subtag}));
return false;
}
// pull all children of importXml[subtag] into obj
@ -259,7 +259,7 @@ export class LDMLKeyboardXMLSourceFileReader {
if (!Array.isArray(subsubval)) {
// This is somewhat of an internal error, indicating that a non-mergeable XML file was imported
// Not exercisable with the standard LDML imports.
this.callbacks.reportMessage(CommonTypesMessages.Error_ImportMergeFail({base, path, subtag, subsubtag}));
this.callbacks.reportMessage(DeveloperUtilsMessages.Error_ImportMergeFail({base, path, subtag, subsubtag}));
return false;
}
// Mark all children as an import
@ -286,7 +286,7 @@ export class LDMLKeyboardXMLSourceFileReader {
public validate(source: LDMLKeyboardXMLSourceFile | LDMLKeyboardTestDataXMLSourceFile): boolean {
if(!SchemaValidators.default.ldmlKeyboard3(source)) {
for (const err of (<any>SchemaValidators.default.ldmlKeyboard3).errors) {
this.callbacks.reportMessage(CommonTypesMessages.Error_SchemaValidationError({
this.callbacks.reportMessage(DeveloperUtilsMessages.Error_SchemaValidationError({
instancePath: err.instancePath,
keyword: err.keyword,
message: err.message || 'Unknown AJV Error', // docs say 'message' is optional if 'messages:false' in options
@ -318,7 +318,7 @@ export class LDMLKeyboardXMLSourceFileReader {
try {
source = this.loadUnboxed(file);
} catch(e) {
this.callbacks.reportMessage(CommonTypesMessages.Error_InvalidXml({e}));
this.callbacks.reportMessage(DeveloperUtilsMessages.Error_InvalidXml({e}));
return null;
}
@ -359,7 +359,7 @@ export class LDMLKeyboardXMLSourceFileReader {
} else if (r.length === 1) {
return r[0];
} else {
this.callbacks.reportMessage(CommonTypesMessages.Error_TestDataUnexpectedArray({subtag}));
this.callbacks.reportMessage(DeveloperUtilsMessages.Error_TestDataUnexpectedArray({subtag}));
return null; // ERROR
}
}

View file

@ -1,9 +1,9 @@
/**
* Helpers and utilities for the Mocha tests.
*/
import * as path from 'path';
import * as fs from "fs";
import { fileURLToPath } from 'url';
import * as path from 'node:path';
import * as fs from "node:fs";
import { fileURLToPath } from 'node:url';
/**
* Builds a path to the fixture with the given path components.

View file

@ -14,7 +14,7 @@ import { makePathToFixture } from '../helpers/index.js';
import { KpsFileReader } from "../../src/types/kps/kps-file-reader.js";
import { KpsFileWriter } from '../../src/types/kps/kps-file-writer.js';
import { CommonTypesMessages } from '../../src/common-messages.js';
import { DeveloperUtilsMessages } from '../../src/developer-utils-messages.js';
const callbacks = new TestCompilerCallbacks();
@ -80,6 +80,6 @@ describe('kps-file-reader', function () {
assert.isNull(kps);
assert.lengthOf(callbacks.messages, 1);
assert.isTrue(callbacks.hasMessage(CommonTypesMessages.ERROR_InvalidPackageFile));
assert.isTrue(callbacks.hasMessage(DeveloperUtilsMessages.ERROR_InvalidPackageFile));
});
});

View file

@ -1,7 +1,7 @@
import 'mocha';
import {assert} from 'chai';
import { DeveloperUtilsMessages } from '../../src/developer-utils-messages.js';
import { Constants } from '@keymanapp/common-types';
import { CommonTypesMessages } from '../../src/common-messages.js';
import { LKKey, ImportStatus } from '../../src/types/ldml-keyboard/ldml-keyboard-xml.js';
import { testReaderCases } from '../helpers/reader-callback-test.js';
@ -19,7 +19,7 @@ describe('ldml keyboard xml reader tests', function () {
testReaderCases([
{
subpath: 'invalid-structure-per-dtd.xml',
errors: [CommonTypesMessages.Error_SchemaValidationError({
errors: [DeveloperUtilsMessages.Error_SchemaValidationError({
instancePath: '/keyboard3',
keyword: 'required',
message: `must have required property 'info'`,
@ -28,7 +28,7 @@ describe('ldml keyboard xml reader tests', function () {
},
{
subpath: 'invalid-conforms-to.xml',
errors: [CommonTypesMessages.Error_SchemaValidationError({
errors: [DeveloperUtilsMessages.Error_SchemaValidationError({
instancePath: '/keyboard3/conformsTo',
keyword: 'enum',
message: `must be equal to one of the allowed values`,
@ -150,7 +150,7 @@ describe('ldml keyboard xml reader tests', function () {
subpath: 'invalid-import-base.xml',
loadfail: true,
errors: [
CommonTypesMessages.Error_ImportInvalidBase({
DeveloperUtilsMessages.Error_ImportInvalidBase({
base: 'SOME_INVALID_BASE',
path: 'B',
subtag: 'C'
@ -161,7 +161,7 @@ describe('ldml keyboard xml reader tests', function () {
subpath: 'invalid-import-local.xml',
loadfail: true,
errors: [
CommonTypesMessages.Error_ImportReadFail({
DeveloperUtilsMessages.Error_ImportReadFail({
base: undefined,
path: 'keys-Zyyy-DOESNOTEXIST.xml',
subtag: 'keys'
@ -172,7 +172,7 @@ describe('ldml keyboard xml reader tests', function () {
subpath: 'invalid-import-path.xml',
loadfail: true,
errors: [
CommonTypesMessages.Error_ImportInvalidPath({
DeveloperUtilsMessages.Error_ImportInvalidPath({
base: 'cldr',
path: '45/too/many/slashes/leading/to/nothing-Zxxx-does-not-exist.xml',
subtag: null,
@ -183,7 +183,7 @@ describe('ldml keyboard xml reader tests', function () {
subpath: 'invalid-import-readfail.xml',
loadfail: true,
errors: [
CommonTypesMessages.Error_ImportReadFail({
DeveloperUtilsMessages.Error_ImportReadFail({
base: 'cldr',
path: '45/none-Zxxx-does-not-exist.xml',
subtag: null,
@ -194,7 +194,7 @@ describe('ldml keyboard xml reader tests', function () {
subpath: 'invalid-import-wrongroot.xml',
loadfail: true,
errors: [
CommonTypesMessages.Error_ImportWrongRoot({
DeveloperUtilsMessages.Error_ImportWrongRoot({
base: null,
path: '45/keys-Zyyy-punctuation.xml',
subtag: 'flicks',

View file

@ -4,7 +4,23 @@
* Copy a keyboard or lexical model project
*/
import { CloudUrls, GitHubUrls, CompilerCallbacks, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KeymanDeveloperProject, KeymanDeveloperProjectOptions, KPJFileReader, KPJFileWriter, KpsFileReader, KpsFileWriter, ValidIds, CompilerBaseOptions } from "@keymanapp/developer-utils";
import {
CloudUrls,
GitHubUrls,
CompilerCallbacks,
KeymanCompiler,
KeymanCompilerArtifact,
KeymanCompilerArtifacts,
KeymanCompilerResult,
KeymanDeveloperProject,
KeymanDeveloperProjectOptions,
ProjectLoader,
KPJFileWriter,
KpsFileReader,
KpsFileWriter,
ValidIds,
CompilerBaseOptions,
} from "@keymanapp/developer-utils";
import { KeymanFileTypes } from "@keymanapp/common-types";
import { CopierMessages } from "./copier-messages.js";
@ -124,7 +140,7 @@ export class KeymanProjectCopier implements KeymanCompiler {
this.sourceId = this.callbacks.path.basename(projectSource, KeymanFileTypes.Source.Project);
// copy project file
const project = await this.loadProjectFromFile(projectSource);
const project = await ProjectLoader.loadProjectFromFile(projectSource, this.callbacks, this.asyncCallbacks);
if(!project) {
// loadProjectFromFile already reported errors
return null;
@ -637,31 +653,6 @@ export class KeymanProjectCopier implements KeymanCompiler {
return this.callbacks.path.join(this.outPath, 'source');
}
private async loadProjectFromFile(filename: string): Promise<KeymanDeveloperProject> {
const kpjData = await this.asyncCallbacks.fsAsync.readFile(filename);
const reader = new KPJFileReader(this.asyncCallbacks);
let kpj = null;
try {
kpj = reader.read(kpjData);
if(!kpj) {
this.callbacks.reportMessage(CopierMessages.Error_ProjectFileCouldNotBeRead({filename}));
return null;
}
if(kpj.KeymanDeveloperProject?.Options?.Version && kpj.KeymanDeveloperProject.Options.Version != "1.0" && kpj.KeymanDeveloperProject.Options.Version != "2.0") {
this.callbacks.reportMessage(CopierMessages.Error_UnsupportedProjectVersion({filename, version: kpj.KeymanDeveloperProject.Options.Version}));
return null;
}
reader.validate(kpj);
} catch(e) {
this.callbacks.reportMessage(CopierMessages.Error_InvalidProjectFile({filename, message: (e ?? '').toString()}));
return null;
}
const project = await reader.transform(filename, kpj);
return project;
}
/**
* Write artifacts from a successful compile to disk, via callbacks methods.
* The artifacts written will include all files from the project, across

View file

@ -92,22 +92,9 @@ export class CopierMessages {
`The package source file '${def(o.filename)}' could not be loaded. The file may have an invalid format`
);
static ERROR_UnsupportedProjectVersion = SevError | 0x000D;
static Error_UnsupportedProjectVersion = (o:{filename: string, version: string}) => m(this.ERROR_UnsupportedProjectVersion,
`Project version ${def(o.version)} for '${def(o.filename)}' is not supported by this version of Keyman Developer`
);
static ERROR_InvalidProjectFile = SevError | 0x000E;
static Error_InvalidProjectFile = (o:{filename: string, message: string}) => m(
this.ERROR_InvalidProjectFile,
`Project file '${def(o.filename)}' is not valid: ${def(o.message)}`
);
static ERROR_ProjectFileCouldNotBeRead = SevError | 0x000F;
static Error_ProjectFileCouldNotBeRead = (o:{filename: string}) => m(
this.ERROR_ProjectFileCouldNotBeRead,
`Project file '${def(o.filename)}' could not be read`
);
// 0x000D ERROR_UnsupportedProjectVersion: moved to developer-utils-messages
// 0x000E ERROR_InvalidProjectFile: moved to developer-utils-messages
// 0x000F ERROR_ProjectFileCouldNotBeRead: moved to developer-utils-messages
static INFO_DryRun = SevInfo | 0x0010;
static Info_DryRun = (o:{outPath: string}) => m(

View file

@ -9,6 +9,7 @@
"paths": {
"@keymanapp/keyman-version": ["../../../common/web/keyman-version/keyman-version.mts"],
"@keymanapp/common-types": ["../../../common/web/types/src/main"],
"@keymanapp/developer-utils": ["../common/web/utils"],
},
},
@ -21,5 +22,6 @@
"references": [
{ "path": "../../../common/web/keyman-version" },
{ "path": "../../../common/web/types/" },
{ "path": "../common/web/utils/" },
]
}

View file

@ -9,8 +9,8 @@
* If you need to add more Unicode blocks, customize SPACELESS_SCRIPT_BLOCKS.
*/
import {readFileSync} from "fs";
import * as path from "path";
import {readFileSync} from "node:fs";
import * as path from "node:path";
// Where to find UnicodeData.txt and Blocks.txt
const UCD_DIR = path.join("..", "..", "..", "..", "resources", "standards-data", "unicode-character-database");

View file

@ -144,7 +144,7 @@ export class PackageCompilerMessages {
`The source file '${def(o.filename)}' should not be included in the package; instead include the compiled result.`
);
// 0x001E was ERROR_InvalidPackageFile, now CommonTypesMessages.Error_InvalidPackageFile
// 0x001E was ERROR_InvalidPackageFile, now DeveloperUtilsMessages.Error_InvalidPackageFile
static ERROR_FileRecordIsMissingName = SevError | 0x001F;
static Error_FileRecordIsMissingName = (o:{description:string}) => m(this.ERROR_FileRecordIsMissingName,
@ -191,7 +191,7 @@ export class PackageCompilerMessages {
`The format for version numbers should be number[.number[.number]]. Each
number component should be an integer, without leading zeroes.`
);
static ERROR_PackageMustNotContainItself = SevError | 0x0028;
static Error_PackageMustNotContainItself = (o:{outputFilename: string}) => m(
this.ERROR_PackageMustNotContainItself, `The package may not include a .kmp file of the same name '${def(o.outputFilename)}'.`, `

View file

@ -8,7 +8,7 @@ import { buildActivities, buildKeyboardInfoActivity, buildModelInfoActivity } fr
import { InfrastructureMessages } from '../../messages/infrastructureMessages.js';
import { loadProject } from '../../util/projectLoader.js';
import { ExtendedCompilerOptions } from '../../util/extendedCompilerOptions.js';
import { getOption } from '@keymanapp/developer-utils';
import { getOption } from '../../util/options.js';
export class BuildProject extends BuildActivity {
public get name(): string { return 'Project'; }

View file

@ -6,7 +6,8 @@
import { Command, Option } from 'commander';
import { declareBuild } from './commands/build.js';
import { declareAnalyze } from './commands/analyze.js';
import { KeymanSentry, loadOptions } from '@keymanapp/developer-utils';
import { loadOptions } from './util/options.js';
import { KeymanSentry } from './util/KeymanSentry.js';
import KEYMAN_VERSION from "@keymanapp/keyman-version";
import { TestKeymanSentry } from './util/TestKeymanSentry.js';
import { exitProcess } from './util/sysexits.js';

View file

@ -38,9 +38,7 @@ export class InfrastructureMessages {
static Info_FileNotBuiltSuccessfully = (o:{filename:string,relativeFilename:string}) => ({filename:o.filename, ...m(this.INFO_FileNotBuiltSuccessfully,
`${def(o.relativeFilename)} failed to build.`)});
static ERROR_InvalidProjectFile = SevError | 0x0008;
static Error_InvalidProjectFile = (o:{message:string}) => m(this.ERROR_InvalidProjectFile,
`Project file is not valid: ${def(o.message)}`);
// 0x0008: ERROR_InvalidProjectFile moved to DeveloperUtilsMessages
static HINT_FilenameHasDifferingCase = SevHint | 0x0009;
static Hint_FilenameHasDifferingCase = (o:{reference:string, filename:string}) => m(this.HINT_FilenameHasDifferingCase,
@ -84,9 +82,7 @@ export class InfrastructureMessages {
static Error_InvalidProjectFolder = (o:{folderName:string}) => m(this.ERROR_InvalidProjectFolder,
`The folder ${def(o.folderName)} does not appear to be a Keyman Developer project.`);
static ERROR_UnsupportedProjectVersion = SevError | 0x0013;
static Error_UnsupportedProjectVersion = (o:{version:string}) => m(this.ERROR_UnsupportedProjectVersion,
`Project version ${def(o.version)} is not supported by this version of Keyman Developer.`);
// 0x0013: ERROR_UnsupportedProjectVersion moved to developer-utils-messages
static HINT_ProjectIsVersion10 = SevHint | 0x0014;
static Hint_ProjectIsVersion10 = () => m(this.HINT_ProjectIsVersion10,

View file

@ -1,4 +1,4 @@
import { CommonTypesMessages, CompilerErrorNamespace } from '@keymanapp/developer-utils';
import { DeveloperUtilsMessages, CompilerErrorNamespace } from '@keymanapp/developer-utils';
import { AnalyzerMessages } from '@keymanapp/kmc-analyze';
import { CopierMessages } from '@keymanapp/kmc-copy';
import { GeneratorMessages } from '@keymanapp/kmc-generate';
@ -13,7 +13,7 @@ import { InfrastructureMessages } from './infrastructureMessages.js';
// Maps every compiler error namespace to the corresponding implementation
const messageNamespaces: Record<CompilerErrorNamespace, any> = {
[CompilerErrorNamespace.LdmlKeyboardCompiler]: LdmlCompilerMessages,
[CompilerErrorNamespace.CommonTypes]: CommonTypesMessages,
[CompilerErrorNamespace.DeveloperUtils]: DeveloperUtilsMessages,
[CompilerErrorNamespace.KmnCompiler]: KmnCompilerMessages,
[CompilerErrorNamespace.ModelCompiler]: ModelCompilerMessages,
[CompilerErrorNamespace.PackageCompiler]: PackageCompilerMessages,
@ -38,7 +38,7 @@ export type CompilerMessageSource = {
// TODO: consolidate with messageNamespaces above
export const messageSources: Record<CompilerErrorNamespace, CompilerMessageSource> = {
[CompilerErrorNamespace.LdmlKeyboardCompiler]: { module: 'kmc-ldml', class: LdmlCompilerMessages },
[CompilerErrorNamespace.CommonTypes]: { module: 'common-types', class: CommonTypesMessages },
[CompilerErrorNamespace.DeveloperUtils]: { module: 'developer-utils', class: DeveloperUtilsMessages },
[CompilerErrorNamespace.KmnCompiler]: { module: 'kmc-kmn', class: KmnCompilerMessages },
[CompilerErrorNamespace.ModelCompiler]: { module: 'kmc-model', class: ModelCompilerMessages },
[CompilerErrorNamespace.PackageCompiler]: { module: 'kmc-package', class: PackageCompilerMessages },

View file

@ -0,0 +1,104 @@
// TODO: this is duplicated in server.js
import Sentry from "@sentry/node";
import KEYMAN_VERSION from "@keymanapp/keyman-version";
import { getOption } from "./options.js";
export type SentryNodeOptions = Sentry.NodeOptions;
/**
* Maximum delay on shutdown of process to send pending events
* to Sentry, in msec
*/
const CLOSE_TIMEOUT = 2000;
let isInit = false;
export class KeymanSentry {
static isEnabled() {
if(process.argv.includes('--no-error-reporting')) {
return false;
}
if(process.argv.includes('--error-reporting')) {
return true;
}
return getOption('automatically report errors', true);
}
static init(options?: SentryNodeOptions) {
options = options ?? {};
Sentry.init({
...options,
dsn: 'https://39b25a09410349a58fe12aaf721565af@o1005580.ingest.sentry.io/5983519', // Keyman Developer
environment: KEYMAN_VERSION.VERSION_ENVIRONMENT,
release: KEYMAN_VERSION.VERSION_GIT_TAG,
});
isInit = true;
}
private static writeSentryMessage(eventId: string) {
process.stderr.write(`
This error has been automatically reported to the Keyman team.
Identifier: ${eventId}
Application: Keyman Developer
Reported at: https://sentry.io/organizations/keyman/projects/keyman-developer/events/${eventId}/
`);
}
static async reportException(e: any, silent: boolean = true) {
if(isInit) {
const eventId = await Sentry.captureException(e);
if(!silent) {
this.writeSentryMessage(eventId);
}
return eventId;
}
return null;
}
static captureMessage(message: string) {
if(isInit) {
return Sentry.captureMessage(message);
} else {
return null;
}
}
/**
* capture an exception for Sentry; note that in local environments, this will normally
* throw the exception rather than sending to Sentry
* @param e
* @param force if true, reports to Sentry even in local environments
*/
static async captureException(e: any, force?: boolean): Promise<never> {
if(isInit) {
// For local development, we don't want to bury the trace; we need the cast to avoid
// TS2367 (comparison appears to be unintentional)
if(!force && (KEYMAN_VERSION.VERSION_ENVIRONMENT as string) == 'local') {
throw e;
}
const eventId = Sentry.captureException(e);
process.stderr.write(`
Fatal error: ${(e??'').toString()}
`);
this.writeSentryMessage(eventId);
await this.close();
process.exit(1);
} else {
throw e;
}
}
static async close() {
if(isInit) {
try {
await Sentry.close(CLOSE_TIMEOUT);
} catch(e) {
// ignore any errors here
}
}
}
}

View file

@ -13,7 +13,7 @@ import { CompilerCallbacks, CompilerEvent,
import { InfrastructureMessages } from '../messages/infrastructureMessages.js';
import chalk from 'chalk';
import supportsColor from 'supports-color';
import { KeymanSentry } from '@keymanapp/developer-utils';
import { KeymanSentry } from './KeymanSentry.js';
import { fileURLToPath } from 'url';
const color = chalk.default;

View file

@ -1,6 +1,6 @@
import { KmnCompiler } from "@keymanapp/kmc-kmn";
import { NodeCompilerCallbacks } from "./NodeCompilerCallbacks.js";
import { KeymanSentry, SentryNodeOptions } from '@keymanapp/developer-utils';
import { KeymanSentry, SentryNodeOptions } from './KeymanSentry.js';
const cli = process.argv.join(' ');

View file

@ -1,6 +1,6 @@
import Sentry from "@sentry/node";
import * as process from "node:process";
import { SentryNodeOptions } from "@keymanapp/developer-utils";
import { SentryNodeOptions } from "./KeymanSentry.js";
Sentry.setContext('Command Line', {
// obfusate parameters with longer paths e.g. from 'c:/users/name/a/b' to

View file

@ -2,7 +2,7 @@
import * as path from 'path';
import * as fs from 'fs';
import { KeymanFileTypes } from "@keymanapp/common-types";
import { CompilerCallbacks, KeymanDeveloperProject, KPJFileReader } from "@keymanapp/developer-utils";
import { CompilerCallbacks, KeymanDeveloperProject, ProjectLoader } from "@keymanapp/developer-utils";
import { InfrastructureMessages } from "../messages/infrastructureMessages.js";
export const isProject = (filename: string): boolean =>
@ -26,7 +26,7 @@ export async function loadProject(infile: string, callbacks: CompilerCallbacks)
}
const project = callbacks.fs.existsSync(infile) ?
await loadProjectFromFile(infile, callbacks) :
await ProjectLoader.loadProjectFromFile(infile, callbacks) :
await loadDefaultProjectFromFolder(infile, callbacks);
return project;
@ -42,21 +42,3 @@ async function loadDefaultProjectFromFolder(infile: string, callbacks: CompilerC
return project;
}
async function loadProjectFromFile(infile: string, callbacks: CompilerCallbacks): Promise<KeymanDeveloperProject> {
const kpjData = await callbacks.fsAsync.readFile(infile);
const reader = new KPJFileReader(callbacks);
let kpj = null;
try {
kpj = reader.read(kpjData);
if(kpj.KeymanDeveloperProject?.Options?.Version && kpj.KeymanDeveloperProject.Options.Version != "1.0" && kpj.KeymanDeveloperProject.Options.Version != "2.0") {
callbacks.reportMessage(InfrastructureMessages.Error_UnsupportedProjectVersion({version: kpj.KeymanDeveloperProject.Options.Version}));
return null;
}
reader.validate(kpj);
} catch(e) {
callbacks.reportMessage(InfrastructureMessages.Error_InvalidProjectFile({message: (e??'').toString()}));
return null;
}
const project = await reader.transform(infile, kpj);
return project;
}

View file

@ -1,4 +1,4 @@
import { KeymanSentry } from "@keymanapp/developer-utils";
import { KeymanSentry } from './KeymanSentry.js';
/**
* Exit codes defined in <sysexits.h>:

View file

@ -1,5 +1,5 @@
import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
import { clearOptions } from '@keymanapp/developer-utils';
import { clearOptions } from '../src/util/options.js';
import { assert } from 'chai';
import 'mocha';
import { BuildProject } from '../src/commands/buildClasses/BuildProject.js';

View file

@ -8,9 +8,9 @@ import { NodeCompilerCallbacks } from '../src/util/NodeCompilerCallbacks.js';
import { KeymanFileTypes } from '@keymanapp/common-types';
import { unitTestEndpoints } from '../src/commands/build.js';
import { KmnCompilerMessages } from '@keymanapp/kmc-kmn';
import { clearOptions } from '@keymanapp/developer-utils';
import { clearOptions } from '../src/util/options.js';
import { loadProject } from '../src/util/projectLoader.js';
import { CompilerErrorNamespace, CompilerEvent, defaultCompilerOptions, CompilerOptions} from '@keymanapp/developer-utils';
import { CompilerErrorNamespace, CompilerEvent, defaultCompilerOptions, CompilerOptions, DeveloperUtilsMessages} from '@keymanapp/developer-utils';
import { analyzeUnitTestEndpoints } from '../src/commands/analyze.js';
import { BuildKeyboardInfo } from '../src/commands/buildClasses/BuildKeyboardInfo.js';
import { BuildModelInfo } from '../src/commands/buildClasses/BuildModelInfo.js';
@ -71,12 +71,12 @@ describe('InfrastructureMessages', function () {
'ERROR_OutFileNotValidForProjects not generated, instead got: '+JSON.stringify(ncb.messages,null,2));
});
// ERROR_InvalidProjectFile
// ERROR_InvalidProjectFile - test stays here for now though referencing developer-messages
it('should generate ERROR_InvalidProjectFile if a project file is invalid', async function() {
const ncb = new NodeCompilerCallbacks({logLevel: 'silent'});
await unitTestEndpoints.build(makePathToFixture('invalid-projects', 'error_invalid_project_file.kpj'), null, ncb, {});
assert.isTrue(ncb.hasMessage(InfrastructureMessages.ERROR_InvalidProjectFile),
assert.isTrue(ncb.hasMessage(DeveloperUtilsMessages.ERROR_InvalidProjectFile),
'ERROR_InvalidProjectFile not generated, instead got: '+JSON.stringify(ncb.messages,null,2));
});
@ -202,14 +202,14 @@ describe('InfrastructureMessages', function () {
assertMessagesEqual(ncb.messages, expectedMessages);
});
// ERROR_UnsupportedProjectVersion
// ERROR_UnsupportedProjectVersion (test stays here for now though referencing developer-utils)
it('should generate ERROR_UnsupportedProjectVersion if a project file with an unsupported version is loaded', async function() {
const ncb = new NodeCompilerCallbacks({logLevel: 'silent'});
const filename = makePathToFixture('invalid-projects', 'error_unsupported_project_version.kpj');
const expectedMessages = [
InfrastructureMessages.INFO_BuildingFile,
InfrastructureMessages.ERROR_UnsupportedProjectVersion,
DeveloperUtilsMessages.ERROR_UnsupportedProjectVersion,
InfrastructureMessages.INFO_ProjectNotBuiltSuccessfully
];
await unitTestEndpoints.build(filename, null, ncb, {compilerWarningsAsErrors: true});

View file

@ -4,7 +4,7 @@ import 'mocha';
import { BuildProject } from '../src/commands/buildClasses/BuildProject.js';
import { makePathToFixture } from './helpers/index.js';
import { InfrastructureMessages } from '../src/messages/infrastructureMessages.js';
import { clearOptions } from '@keymanapp/developer-utils';
import { clearOptions } from '../src/util/options.js';
const callbacks = new TestCompilerCallbacks();

View file

@ -1,3 +1,4 @@
// TODO: this is duplicated in kmc!
import Sentry from "@sentry/node";
import KEYMAN_VERSION from "@keymanapp/keyman-version";
import { getOption } from "./options.js";

View file

@ -5,12 +5,13 @@ import chalk from 'chalk';
import express from 'express';
import multer from 'multer';
import * as ws from 'ws';
import { KeymanSentry, loadOptions } from '@keymanapp/developer-utils';
import { KeymanSentry } from './KeymanSentry.js';
import { configuration } from './config.js';
import { environment } from './environment.js';
import setupRoutes from './routes.js';
import { shutdown } from './shutdown.js';
import { initTray } from './tray.js';
import { loadOptions } from './options.js';
const options = {
ngrokLog: false, // Set this to true if you need to see ngrok logs in the console

View file

@ -1,4 +1,4 @@
import { KeymanSentry } from '@keymanapp/developer-utils';
import { KeymanSentry } from './KeymanSentry.js';
import { readFileSync, existsSync } from 'fs';
export function loadJsonFile(filename: string): any {

View file

@ -0,0 +1,96 @@
// TODO: this is duplicated in kmc
import * as os from 'node:os';
import * as fs from 'node:fs';
import * as path from 'node:path';
export interface KeymanDeveloperOptions {
"use tab char"?: boolean;
"link font sizes"?: boolean;
"indent size"?: number;
"use old debugger"?: boolean;
"editor theme"?: string;
"debugger break when exiting line"?: boolean;
"debugger single step after break"?: boolean;
"debugger show store offset"?: boolean;
"debugger recompile with debug info"?: boolean;
"debugger auto reset before compilng"?: boolean;
"auto save before compiling"?: boolean;
"osk auto save before importing"?: boolean;
"web host port"?: number;
"server keep alive"?: boolean;
"server use local addresses"?: boolean;
"server ngrok token"?: string;
"server ngrok region"?: string;
"server use ngrok"?: boolean;
"server show console window"?: boolean;
"char map disable database lookups"?: boolean;
"char map auto lookup"?: boolean;
"open keyboard files in source view"?: boolean;
"display theme"?: string;
"external editor path"?: string;
"smtp server"?: string;
"test email addresses"?: string;
"web ladder length"?: number;
"default project path"?: string;
"automatically report errors"?: boolean;
"automatically report usage"?: boolean;
"toolbar visible"?: boolean;
"active project"?: string;
"prompt to upgrade projects"?: boolean;
};
type KeymanDeveloperOption = keyof KeymanDeveloperOptions;
// Default has no options set, and unit tests will use the defaults (won't call
// `loadOptions()`)
let options: KeymanDeveloperOptions = {};
// We only load the options from disk once on first use
let optionsLoaded = false;
export async function loadOptions(): Promise<KeymanDeveloperOptions> {
if(optionsLoaded) {
return options;
}
options = {};
try {
const optionsFile = path.join(os.homedir(), '.keymandeveloper', 'options.json');
if(fs.existsSync(optionsFile)) {
for(let i = 0; i < 5; i++) {
try {
const data = JSON.parse(fs.readFileSync(optionsFile, 'utf-8'));
if(typeof data == 'object') {
options = data;
}
break;
} catch(e) {
if(e?.code == 'EBUSY') {
await new Promise(resolve => setTimeout(resolve, 500));
} else {
// give up, nothing to report here
break;
}
}
}
}
} catch(e) {
// Nothing to report here, sadly -- because we cannot rely on Sentry at this
// low level.
options = {};
}
optionsLoaded = true;
return options;
}
export function getOption<T extends KeymanDeveloperOption>(valueName: T, defaultValue: KeymanDeveloperOptions[T]): KeymanDeveloperOptions[T] {
return options[valueName] ?? defaultValue;
}
/**
* unit tests will clear options before running, for consistency
*/
export function clearOptions() {
options = {};
optionsLoaded = true;
}

View file

@ -1,4 +1,4 @@
import { KeymanSentry } from '@keymanapp/developer-utils';
import { KeymanSentry } from './KeymanSentry.js';
export async function shutdown(code?: number): Promise<never> {
await KeymanSentry.close();

View file

@ -346,7 +346,7 @@ LPKMX_KEYBOARD CopyKeyboard(PKMX_BYTE bufp, PKMX_BYTE base) {
* @return pointer to the keyboard
*/
LPKMX_KEYBOARD KMX_FixupKeyboard(PKMX_BYTE bufp, PKMX_BYTE base, KMX_DWORD dwFileSize) {
UNREFERENCED_PARAMETER(dwFileSize);
// UNREFERENCED_PARAMETER(dwFileSize);
KMX_DWORD i, j;
PCOMP_KEYBOARD ckbp = (PCOMP_KEYBOARD)base;
@ -369,9 +369,9 @@ LPKMX_KEYBOARD KMX_FixupKeyboard(PKMX_BYTE bufp, PKMX_BYTE base, KMX_DWORD dwFil
for (gp = kbp->dpGroupArray, cgp = (PCOMP_GROUP)gp, i = 0; i < kbp->cxGroupArray; i++, gp++, cgp++) {
gp->dpName = KMX_StringOffset(base, cgp->dpName);
gp->dpKeyArray = (LPKMX_KEY)(base + cgp->dpKeyArray);
if (cgp->dpMatch != NULL)
if (cgp->dpMatch != 0)
gp->dpMatch = (PKMX_WCHAR)(base + cgp->dpMatch);
if (cgp->dpNoMatch != NULL)
if (cgp->dpNoMatch != 0)
gp->dpNoMatch = (PKMX_WCHAR)(base + cgp->dpNoMatch);
for (kp = gp->dpKeyArray, ckp = (PCOMP_KEY)kp, j = 0; j < gp->cxKeyArray; j++, kp++, ckp++) {

View file

@ -3,7 +3,7 @@ import {
} from '@actions/core';
import { GitHub } from '@actions/github';
import { readFileSync, writeFileSync } from 'fs';
import { readFileSync, writeFileSync } from 'node:fs';
import { gt } from 'semver';
import { reportHistory } from './reportHistory.js';
import { spawnChild } from './util/spawnAwait.js';

View file

@ -1,11 +1,11 @@
import { readFileSync, writeFileSync } from 'fs';
import { readFileSync, writeFileSync } from 'node:fs';
// ------------------------------------------------------------------------------------
// incrementVersion
// ------------------------------------------------------------------------------------
export const incrementVersion = () => {
// 1.
// 1.
const version = readFileSync('./VERSION.md', 'utf8');
const triplet = version.split('.');
const patch = parseInt(triplet[2], 10) + 1;

View file

@ -5,7 +5,7 @@ import { sendCommentToPullRequestAndRelatedIssues, fixupHistory } from './fixupH
import { incrementVersion } from './incrementVersion.js';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { readFileSync } from 'fs';
import { readFileSync } from 'node:fs';
import { reportHistory } from './reportHistory.js';
const argv = await yargs(hideBin(process.argv))

View file

@ -7,9 +7,9 @@ import {
default as CloudQueryEngine
} from './cloud/queryEngine.js';
import fs from 'fs';
import https from 'https';
import vm from 'vm';
import fs from 'node:fs';
import https from 'node:https';
import vm from 'node:vm';
export default class NodeCloudRequester implements CloudRequesterInterface {
private static QUERY_SEED = 1;

View file

@ -2,8 +2,8 @@
// Original version found at: https://github.com/eddieantonio/unicode-default-word-boundary/blob/master/libexec/compile-word-break.js
import fs from 'fs';
import path from 'path';
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);

View file

@ -3,17 +3,17 @@
/// <reference path="../worker-interface.d.ts" />
// Defines types related to Node workers.
import * as worker from 'worker_threads';
import { Buffer } from 'buffer';
import { URL } from 'url';
import * as worker from 'node:worker_threads';
import { Buffer } from 'node:buffer';
import { URL } from 'node:url';
/**
* Defines mappings from Node Worker signatures to WebWorker signatures
*/
const nodeWorkerToWebWorkerMappingSource = `
import { parentPort } from 'worker_threads';
import fs from 'fs';
import vm from 'vm';
import { parentPort } from 'node:worker_threads';
import fs from 'node:fs';
import vm from 'node:vm';
function postMessage(...args) {
parentPort.postMessage.call(parentPort, args);

View file

@ -1,7 +1,7 @@
import SourcemapRemapper from "@keymanapp/sourcemap-path-remapper"
import convertSourceMap from 'convert-source-map'; // Transforms sourcemaps among various common formats.
// Base64, stringified-JSON, end-of-file comment...
import fs from 'fs';
import fs from 'node:fs';
function displayHelp() {
console.log("KeymanWeb's sourcemap-cleansing tool. This tool is designed to produce clean filepaths");