Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 235 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
env: {
node: true,
mocha: true,
es6: true
},
parserOptions: {
project: ['./tsconfig.json'],
createDefaultProgram: true
},
plugins: [
'@typescript-eslint',
'no-only-tests',
'jsdoc',
'import'
],
extends: [
'eslint:all',
'plugin:@typescript-eslint/all',
'plugin:jsdoc/recommended',
'plugin:import/typescript'
],
settings: {
'import/resolver': {
typescript: true,
node: true
}
},
rules: {
'@typescript-eslint/array-type': 'off',
'@typescript-eslint/consistent-type-assertions': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-member-accessibility': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/init-declarations': 'off',
'@typescript-eslint/parameter-properties': 'off',
'@typescript-eslint/lines-between-class-members': 'off',
'@typescript-eslint/member-ordering': 'off',
'@typescript-eslint/method-signature-style': 'off',
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/no-base-to-string': 'off',
'@typescript-eslint/no-confusing-void-expression': 'off',
'@typescript-eslint/no-dynamic-delete': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-extra-parens': 'off',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-implicit-any-catch': 'off',
'@typescript-eslint/no-invalid-this': 'off',
'@typescript-eslint/no-magic-numbers': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-parameter-properties': 'off',
'@typescript-eslint/no-restricted-imports': ['off', {}],
'@typescript-eslint/no-unsafe-argument': 'off',
'object-curly-spacing': 'off',
'@typescript-eslint/object-curly-spacing': [
'error',
'always'
],
'@typescript-eslint/no-shadow': 'off',
'@typescript-eslint/no-this-alias': 'off',
'@typescript-eslint/no-invalid-void': 'off',
'@typescript-eslint/no-invalid-void-type': 'off',
'@typescript-eslint/no-type-alias': 'off',
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off',
'@typescript-eslint/no-unnecessary-condition': 'off',
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars-experimental': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/prefer-for-of': 'error',
'@typescript-eslint/prefer-readonly': 'off',
'@typescript-eslint/prefer-readonly-parameter-types': 'off',
'@typescript-eslint/promise-function-async': 'off',
'@typescript-eslint/quotes': [
'error',
'single',
{
'allowTemplateLiterals': true
}
],
'@typescript-eslint/require-array-sort-compare': 'off',
'@typescript-eslint/restrict-plus-operands': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/sort-type-union-intersection-members': 'off',
'@typescript-eslint/space-before-function-paren': 'off',
'@typescript-eslint/space-infix-ops': 'off',
'@typescript-eslint/strict-boolean-expressions': 'off',
'@typescript-eslint/typedef': 'off',
'@typescript-eslint/unbound-method': 'off',
'@typescript-eslint/unified-signatures': 'off',
'@typescript-eslint/indent': 'off',
'@typescript-eslint/comma-dangle': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'off',
'@typescript-eslint/sort-type-constituents': 'off',
'@typescript-eslint/lines-around-comment': 'off',
'@typescript-eslint/no-duplicate-imports': 'off',
'no-duplicate-imports': 'off',
'jsdoc/require-param': 'off',
'jsdoc/require-returns': 'off',
'jsdoc/require-param-type': 'off',
'jsdoc/newline-after-description': 'off',
'jsdoc/require-jsdoc': 'off',
'jsdoc/require-returns-type': 'off',
'array-bracket-newline': 'off',
'array-element-newline': 'off',
'array-type': 'off',
'arrow-body-style': 'off',
'arrow-parens': 'off',
'callback-return': 'off',
'capitalized-comments': 'off',
'class-methods-use-this': 'off',
'complexity': 'off',
'consistent-return': 'off',
'consistent-this': 'off',
'curly': 'error',
'default-case': 'off',
'dot-location': 'off',
'dot-notation': 'off',
'func-style': 'off',
'function-call-argument-newline': 'off',
'function-paren-newline': 'off',
'getter-return': 'off',
'guard-for-in': 'off',
'id-length': 'off',
'import/no-extraneous-dependencies': ['error', {
'devDependencies': ['**/*.spec.ts', 'scripts/**/*.ts']
}],
'indent': 'off',
'init-declarations': 'off',
'line-comment-position': 'off',
'linebreak-style': 'off',
'lines-around-comment': 'off',
'lines-between-class-members': 'off',
'max-classes-per-file': 'off',
'max-depth': 'off',
'max-len': 'off',
'max-lines': 'off',
'max-lines-per-function': 'off',
'max-params': 'off',
'max-statements': 'off',
'no-only-tests/no-only-tests': 'error',
'multiline-comment-style': 'off',
'multiline-ternary': 'off',
'new-cap': 'off',
'newline-per-chained-call': 'off',
'no-await-in-loop': 'off',
'no-case-declarations': 'off',
'no-constant-condition': 'off',
'no-console': 'off',
'no-continue': 'off',
'no-else-return': 'off',
'no-empty': 'off',
'no-implicit-coercion': 'off',
'no-inline-comments': 'off',
'no-invalid-this': 'off',
'no-labels': 'off',
'no-lonely-if': 'off',
'no-negated-condition': 'off',
'no-param-reassign': 'off',
'no-plusplus': 'off',
'no-process-exit': 'off',
'no-prototype-builtins': 'off',
'no-shadow': 'off',
'no-sync': 'off',
'no-ternary': 'off',
'no-undefined': 'off',
'no-underscore-dangle': 'off',
'no-unneeded-ternary': 'off',
'no-useless-escape': 'off',
'no-void': 'off',
'no-warning-comments': 'off',
'object-property-newline': 'off',
'object-shorthand': [
'error',
'never'
],
'one-var': [
'error',
'never'
],
'padded-blocks': 'off',
'prefer-const': 'off',
'prefer-destructuring': 'off',
'prefer-named-capture-group': 'off',
'prefer-template': 'off',
'quote-props': 'off',
'radix': 'off',
'require-atomic-updates': 'off',
'require-unicode-regexp': 'off',
'sort-imports': 'off',
'sort-keys': 'off',
'spaced-comment': 'off',
'space-infix-ops': 'off',
'vars-on-top': 'off',
'wrap-regex': 'off'
},
overrides: [
{
files: ['*.spec.ts'],
rules: {
// `import x = require()` is the correct TS syntax for `export =` modules
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars-experimental': 'off',
'@typescript-eslint/dot-notation': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'import/no-extraneous-dependencies': 'off',
'func-names': 'off',
'new-cap': 'off',
'no-shadow': 'off',
'no-void': 'off'
}
},
{
files: ['scripts/**/*.ts'],
rules: {
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-require-imports': 'off',
'camelcase': 'off'
}
}
]
};
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ jobs:
strategy: move
- run: npm ci --cache .npm
- run: npm run build
- run: npm run lint
- run: npm run test
- run: npm run test-plugin
- run: npm run test-package
15 changes: 15 additions & 0 deletions .mocharc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const nodeVersion = +process.versions.node.split('.')[0];
const config = {
spec: 'plugin/**/*.spec.ts',
require: [
'source-map-support/register',
'ts-node/register'
],
fullTrace: true,
timeout: 2000,
watchExtensions: ['ts']
};
if (nodeVersion >= 22) {
config['node-option'] = ['no-experimental-strip-types'];
}
module.exports = config;
109 changes: 109 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,115 @@ end function
## How it works
While the promise spec is interoperable with any other promise node created by other libraries, the `promises` namespace is the true magic of the @rokucommunity/promises library. We have several helper functions that enable you to chain multiple promises together, very much in the same way as javascript promises.

## BrighterScript Plugin

`@rokucommunity/promises` ships with an optional [BrighterScript](https://github.com/rokucommunity/brighterscript) plugin that adds compile-time diagnostics to catch a common mistake: passing a `context` argument to `onThen`, `onCatch`, `onFinally`, or the chain API but forgetting to add the corresponding parameter to the inline callback.

### What it catches

```brighterscript
' BAD — context is passed but the callback has no parameter to receive it
promises.onThen(promise, function(response)
print response
end function, context)

' GOOD — the callback declares a second parameter to receive context
promises.onThen(promise, function(response, ctx)
print response, ctx.username
end function, context)
```

The same check applies to `onCatch`, `onFinally`, and the chain API:

```brighterscript
' BAD — chain has context but .then callback only takes one param
promises.chain(usernamePromise, context).then(function(response)
print response
end function)

' GOOD
promises.chain(usernamePromise, context).then(function(response, ctx)
print response, ctx
end function)
```

> **Note:** The plugin only flags *inline* `function`/`sub` literals for the context-param check. Callbacks passed as variable references are skipped — it can't statically verify their signature.

### Missing `.toPromise()` on return (PRMS1002)

The plugin also warns when a chain builder is returned from a function without calling `.toPromise()` first. The chain builder is a plain AA — returning it instead of the underlying Promise node is almost always a bug:

```brighterscript
' BAD — returns the chain builder AA, not a Promise node
function loadData() as object
return promises.chain(fetchData()).then(function(result)
return promises.resolve(result.items)
end function)
end function

' GOOD — .toPromise() unwraps the chain back to a Promise
function loadData() as object
return promises.chain(fetchData()).then(function(result)
return promises.resolve(result.items)
end function).toPromise()
end function
```

This also catches fire-and-forget chains stored in variables that are accidentally returned, and sub-chains returned from inside a `.then()` callback.

### Setup

**1. Add a plugin loader file** (e.g. `bsplugin-promises.js`) to your project root:

```js
const promisesPlugin = require('@rokucommunity/promises/plugin');
module.exports = promisesPlugin();
```

**2. Reference it from `bsconfig.json`:**

```json
{
"plugins": ["./bsplugin-promises.js"]
}
```

That's it. The plugin auto-detects the ropm alias by scanning `roku_modules/` — no further configuration needed for most projects.

### ropm alias detection

When you install via ropm with a custom alias (e.g. `npx ropm install promises@npm:@rokucommunity/promises --alias myProm`), the plugin detects it automatically by looking for `roku_modules/<alias>/source/promises.brs` in your project files. All comparisons are case-insensitive, matching BrightScript's own behavior.

If auto-detection doesn't work for your setup (e.g. non-standard install path), supply the alias explicitly:

```js
// bsplugin-promises.js
const promisesPlugin = require('@rokucommunity/promises/plugin');
module.exports = promisesPlugin({ alias: 'myProm' });
```

Multiple aliases are also supported:

```js
module.exports = promisesPlugin({ alias: ['promises', 'myProm'] });
```

### Diagnostic codes

| Code | Constant | Description |
|------|----------|-------------|
| `PRMS1001` | `DiagnosticCode.ContextParamMissing` | Context passed but inline callback has no parameter to receive it |
| `PRMS1002` | `DiagnosticCode.ChainMissingToPromise` | Chain result returned without `.toPromise()` |

You can suppress specific codes in `bsconfig.json`:

```json
{
"diagnosticFilters": [
{ "src": "source/legacy/**/*.bs", "codes": ["PRMS1001", "PRMS1002"] }
]
}
```

## Limitations
### no support for roMessagePort
Expand Down
Loading
Loading