@tehw0lf/yaft 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FeatureToggle.d.ts +20 -0
- package/FeatureToggle.js +72 -0
- package/README.md +25 -3
- package/evaluate.d.ts +42 -0
- package/evaluate.js +102 -0
- package/examples/ApiServiceBooleanProvider.d.ts +11 -0
- package/examples/ApiServiceBooleanProvider.js +85 -0
- package/examples/ApiServiceFeatureProvider.d.ts +17 -0
- package/examples/ApiServiceFeatureProvider.js +54 -0
- package/examples/LocalStorageBooleanProvider.d.ts +7 -0
- package/examples/LocalStorageBooleanProvider.js +23 -0
- package/examples/LocalStorageFeatureProvider.d.ts +14 -0
- package/examples/LocalStorageFeatureProvider.js +31 -0
- package/index.d.ts +3 -0
- package/index.js +14 -0
- package/mapping.d.ts +35 -0
- package/mapping.js +98 -0
- package/package.json +21 -9
- package/.github/workflows/build.yml +0 -28
- package/.github/workflows/security-scan.yml +0 -79
- package/CLAUDE.md +0 -151
- package/LICENSE +0 -21
- package/conformance.lock +0 -2
- package/jest.config.js +0 -16
- package/scripts/fetch-conformance.sh +0 -67
- package/src/FeatureToggle.ts +0 -95
- package/src/evaluate.ts +0 -122
- package/src/examples/ApiServiceBooleanProvider.ts +0 -82
- package/src/examples/ApiServiceFeatureProvider.ts +0 -52
- package/src/examples/LocalStorageBooleanProvider.ts +0 -25
- package/src/examples/LocalStorageFeatureProvider.ts +0 -31
- package/src/index.ts +0 -8
- package/src/mapping.ts +0 -92
- package/src/test/conformance-adapter/cases.ts +0 -104
- package/src/test/conformance-adapter/decorator.spec.ts +0 -376
- package/src/test/conformance-adapter/evaluation.spec.ts +0 -31
- package/src/test/conformance-adapter/mapping.spec.ts +0 -79
- package/src/test/decorator-behavior.spec.ts +0 -359
- package/src/test/error-handling.spec.ts +0 -396
- package/src/test/evaluate.spec.ts +0 -260
- package/src/test/feature-data.spec.ts +0 -102
- package/src/test/injectable-clock.spec.ts +0 -112
- package/src/test/integration.spec.ts +0 -472
- package/src/test/providers.spec.ts +0 -218
- package/src/test/test-boolean.json +0 -1
- package/src/test/test-feature.json +0 -20
- package/src/test/test-setup.ts +0 -2
- package/src/test/time-based-logic.spec.ts +0 -169
- package/src/test/yaft.demo.spec.ts +0 -53
- package/tsconfig.json +0 -14
- package/tsconfig.spec.json +0 -10
package/mapping.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normaliseFeature = normaliseFeature;
|
|
4
|
+
exports.normaliseCollection = normaliseCollection;
|
|
5
|
+
exports.normaliseBooleans = normaliseBooleans;
|
|
6
|
+
/**
|
|
7
|
+
* Reads a field by presence, not by truthiness.
|
|
8
|
+
*
|
|
9
|
+
* The obvious `raw.value || raw.Value` is wrong: a present but empty value
|
|
10
|
+
* falls through to the other spelling, so a feature stored as `""` reads as
|
|
11
|
+
* whatever the capitalised field holds. An off feature then reports on. The
|
|
12
|
+
* same trap applies to `tags: []`.
|
|
13
|
+
*
|
|
14
|
+
* Backends from 0.2.0 on send only the lowercase spelling; the capitalised one
|
|
15
|
+
* is read because instances before that are still around.
|
|
16
|
+
*/
|
|
17
|
+
function field(raw, lower, upper) {
|
|
18
|
+
if (lower in raw)
|
|
19
|
+
return raw[lower];
|
|
20
|
+
if (upper in raw)
|
|
21
|
+
return raw[upper];
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Normalises a date field. The backend sends `null` for an unset bound and
|
|
26
|
+
* local fixtures use `""`; both mean "no bound", and `evaluate` ignores either.
|
|
27
|
+
*/
|
|
28
|
+
function date(value) {
|
|
29
|
+
return typeof value === 'string' ? value : '';
|
|
30
|
+
}
|
|
31
|
+
/** Normalises one entry into a `Feature`, whichever spelling it arrived in. */
|
|
32
|
+
function normaliseFeature(raw) {
|
|
33
|
+
const tags = field(raw, 'tags', 'Tags');
|
|
34
|
+
return {
|
|
35
|
+
key: String(field(raw, 'key', 'Key') ?? ''),
|
|
36
|
+
value: String(field(raw, 'value', 'Value') ?? ''),
|
|
37
|
+
activeAt: date(field(raw, 'activeAt', 'ActiveAt')),
|
|
38
|
+
disabledAt: date(field(raw, 'disabledAt', 'DisabledAt')),
|
|
39
|
+
// Filtered rather than asserted: `as string[]` is a compile-time claim
|
|
40
|
+
// that a backend sending a mixed array would quietly break, handing
|
|
41
|
+
// callers a non-string through a field typed as string.
|
|
42
|
+
tags: Array.isArray(tags)
|
|
43
|
+
? tags.filter((tag) => typeof tag === 'string')
|
|
44
|
+
: [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Normalises a whole response into features keyed by their key.
|
|
49
|
+
*
|
|
50
|
+
* Three envelopes are accepted, because the backend uses all three:
|
|
51
|
+
*
|
|
52
|
+
* - `{ "toggles": [...] }` for a UUID group;
|
|
53
|
+
* - `{ "value": [...] }`, the same thing under a different name;
|
|
54
|
+
* - a flat object for a single toggle.
|
|
55
|
+
*
|
|
56
|
+
* An entry without a usable key is skipped rather than stored under `""`,
|
|
57
|
+
* where `isEnabled("")` could reach it.
|
|
58
|
+
*/
|
|
59
|
+
function normaliseCollection(response) {
|
|
60
|
+
if (response === null || typeof response !== 'object')
|
|
61
|
+
return {};
|
|
62
|
+
const body = response;
|
|
63
|
+
const collection = Array.isArray(body['toggles'])
|
|
64
|
+
? body['toggles']
|
|
65
|
+
: Array.isArray(body['value'])
|
|
66
|
+
? body['value']
|
|
67
|
+
: undefined;
|
|
68
|
+
// A single toggle comes back flat, not wrapped. Older code only handled the
|
|
69
|
+
// collections and dropped this shape entirely.
|
|
70
|
+
const entries = collection ?? [body];
|
|
71
|
+
const data = {};
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (entry === null || typeof entry !== 'object')
|
|
74
|
+
continue;
|
|
75
|
+
const feature = normaliseFeature(entry);
|
|
76
|
+
if (feature.key !== '')
|
|
77
|
+
data[feature.key] = feature;
|
|
78
|
+
}
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Normalises a boolean-shape payload, `{ "myToggle": true }`.
|
|
83
|
+
*
|
|
84
|
+
* Only real booleans are kept (R29). Anything else is dropped, so its key
|
|
85
|
+
* reads as missing and therefore off. Keeping it and testing its truthiness
|
|
86
|
+
* would turn `"false"` on, since a non-empty string is truthy.
|
|
87
|
+
*/
|
|
88
|
+
function normaliseBooleans(response) {
|
|
89
|
+
if (response === null || typeof response !== 'object' || Array.isArray(response)) {
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
const data = {};
|
|
93
|
+
for (const [key, value] of Object.entries(response)) {
|
|
94
|
+
if (typeof value === 'boolean')
|
|
95
|
+
data[key] = value;
|
|
96
|
+
}
|
|
97
|
+
return data;
|
|
98
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tehw0lf/yaft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "YaFT - Feature Toggles using Go&PostgreSQL or any source!",
|
|
5
5
|
"type": "commonjs",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"default": "./index.js"
|
|
12
|
+
},
|
|
13
|
+
"./examples/*": {
|
|
14
|
+
"types": "./examples/*.d.ts",
|
|
15
|
+
"default": "./examples/*.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
6
19
|
"scripts": {
|
|
7
20
|
"build": "tsc && cp package.json README.md logo.svg dist/yaft/ && npm pkg delete devDependencies --prefix dist/yaft",
|
|
8
21
|
"test": "npx jest",
|
|
@@ -28,13 +41,12 @@
|
|
|
28
41
|
"dependencies": {
|
|
29
42
|
"reflect-metadata": "0.2.2"
|
|
30
43
|
},
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"typescript": "6.0.3"
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"axios": ">=1.0.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"axios": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
39
51
|
}
|
|
40
52
|
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
name: build and publish pipeline
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
pull_request:
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
build:
|
|
11
|
-
name: external workflow
|
|
12
|
-
uses: tehw0lf/workflows/.github/workflows/build-test-publish.yml@main
|
|
13
|
-
permissions:
|
|
14
|
-
actions: write
|
|
15
|
-
attestations: write
|
|
16
|
-
contents: write
|
|
17
|
-
packages: write
|
|
18
|
-
id-token: write
|
|
19
|
-
security-events: write
|
|
20
|
-
pull-requests: write
|
|
21
|
-
with:
|
|
22
|
-
tool: "npm"
|
|
23
|
-
test: "run test"
|
|
24
|
-
build_branch: "run build"
|
|
25
|
-
build_main: "run build"
|
|
26
|
-
artifact_path: "dist"
|
|
27
|
-
library_path: "dist"
|
|
28
|
-
npm_audit_omit_dev: true
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# Security Monitoring Workflow
|
|
2
|
-
#
|
|
3
|
-
# This workflow performs security scanning using only the security-specific
|
|
4
|
-
# reusable workflows, without building or publishing.
|
|
5
|
-
#
|
|
6
|
-
# Schedule: Runs daily at 2:00 AM UTC to catch newly disclosed vulnerabilities
|
|
7
|
-
# Manual Trigger: Can be triggered via GitHub Actions UI or gh CLI
|
|
8
|
-
#
|
|
9
|
-
# Security Checks:
|
|
10
|
-
# - Source code scanning: Semgrep (OWASP Top 10, security-audit, CI), npm audit
|
|
11
|
-
# - Artifact scanning: Trivy on dist/ artifacts (requires prior build)
|
|
12
|
-
#
|
|
13
|
-
name: Security Monitoring
|
|
14
|
-
|
|
15
|
-
on:
|
|
16
|
-
schedule:
|
|
17
|
-
# Run daily at 2:00 AM UTC
|
|
18
|
-
- cron: "0 2 * * *"
|
|
19
|
-
workflow_dispatch:
|
|
20
|
-
# Allow manual triggering
|
|
21
|
-
|
|
22
|
-
permissions:
|
|
23
|
-
contents: read
|
|
24
|
-
security-events: write
|
|
25
|
-
actions: read
|
|
26
|
-
pull-requests: write
|
|
27
|
-
|
|
28
|
-
jobs:
|
|
29
|
-
# Scan source code for security vulnerabilities
|
|
30
|
-
scan_source:
|
|
31
|
-
name: Scan Source Code
|
|
32
|
-
uses: tehw0lf/workflows/.github/workflows/security-scan-source.yml@main
|
|
33
|
-
permissions:
|
|
34
|
-
contents: write
|
|
35
|
-
security-events: write
|
|
36
|
-
actions: read
|
|
37
|
-
pull-requests: write
|
|
38
|
-
with:
|
|
39
|
-
tool: "npm"
|
|
40
|
-
semgrep_rules: "auto p/security-audit p/owasp-top-ten p/ci p/nodejs p/typescript"
|
|
41
|
-
npm_audit_omit_dev: true
|
|
42
|
-
npm_audit_severity_threshold: "high"
|
|
43
|
-
|
|
44
|
-
# Download artifacts from latest CI build
|
|
45
|
-
# Note: Reuses artifacts from the main CI workflow instead of rebuilding
|
|
46
|
-
download_artifacts:
|
|
47
|
-
name: Download Build Artifacts
|
|
48
|
-
runs-on: ubuntu-latest
|
|
49
|
-
|
|
50
|
-
steps:
|
|
51
|
-
- name: Download latest dist artifacts from CI
|
|
52
|
-
uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11 # v6
|
|
53
|
-
with:
|
|
54
|
-
workflow: build.yml
|
|
55
|
-
name: dist
|
|
56
|
-
path: dist/
|
|
57
|
-
if_no_artifact_found: warn
|
|
58
|
-
|
|
59
|
-
- name: Upload artifacts for scanning
|
|
60
|
-
if: hashFiles('dist/**/*') != ''
|
|
61
|
-
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
62
|
-
with:
|
|
63
|
-
name: build
|
|
64
|
-
path: dist/
|
|
65
|
-
retention-days: 1
|
|
66
|
-
|
|
67
|
-
# Scan built artifacts for vulnerabilities
|
|
68
|
-
scan_artifacts:
|
|
69
|
-
name: Scan Artifacts
|
|
70
|
-
needs: download_artifacts
|
|
71
|
-
if: needs.download_artifacts.result == 'success'
|
|
72
|
-
uses: tehw0lf/workflows/.github/workflows/security-scan-artifacts.yml@main
|
|
73
|
-
permissions:
|
|
74
|
-
contents: read
|
|
75
|
-
security-events: write
|
|
76
|
-
actions: read
|
|
77
|
-
with:
|
|
78
|
-
tool: "npm"
|
|
79
|
-
artifact_path: "dist"
|
package/CLAUDE.md
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Project Overview
|
|
6
|
-
|
|
7
|
-
YaFT (Yet another Feature Toggle) is a TypeScript library that provides decorator-based feature toggles for classes and methods. It integrates with backend services (Go/PostgreSQL by default) or local data sources to enable/disable features at runtime.
|
|
8
|
-
|
|
9
|
-
## Development Commands
|
|
10
|
-
|
|
11
|
-
### Building
|
|
12
|
-
```bash
|
|
13
|
-
npm run build # Compile TypeScript and prepare package for publishing
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
### Testing
|
|
17
|
-
```bash
|
|
18
|
-
npm test # Run Jest tests with ts-jest transform
|
|
19
|
-
npx jest # Alternative test command
|
|
20
|
-
npx jest --watch # Run tests in watch mode
|
|
21
|
-
npx jest --coverage # Run tests with coverage report
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
### Running Single Tests
|
|
25
|
-
```bash
|
|
26
|
-
npx jest yaft.spec.ts # Run specific test file
|
|
27
|
-
npx jest -t "test name" # Run specific test by name pattern
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Architecture
|
|
31
|
-
|
|
32
|
-
### Core Components
|
|
33
|
-
|
|
34
|
-
**FeatureToggle Decorator (`src/FeatureToggle.ts`)**
|
|
35
|
-
- Main decorator function that can be applied to classes or methods
|
|
36
|
-
- Evaluates feature state at decoration time using the configured provider
|
|
37
|
-
- Returns fallback implementations when features are disabled
|
|
38
|
-
|
|
39
|
-
**FeatureToggleBase Class**
|
|
40
|
-
- Static container for the global `featureProvider` instance
|
|
41
|
-
- Must be configured before using any `@FeatureToggle` decorators
|
|
42
|
-
|
|
43
|
-
**FeatureProvider Interface**
|
|
44
|
-
- Defines contract for data sources: `isEnabled(key: string): boolean`
|
|
45
|
-
- Optional API integration with `apiUrl`, `baseUUID`, and collection hash support
|
|
46
|
-
- Flexible data structure support via generic type parameter
|
|
47
|
-
|
|
48
|
-
### Provider Implementations
|
|
49
|
-
|
|
50
|
-
**API Providers** (`src/examples/ApiService*Provider.ts`)
|
|
51
|
-
- Integrate with YaFT Go backend via HTTP API
|
|
52
|
-
- Support collection hash for efficient cache invalidation
|
|
53
|
-
- Handle both boolean and Feature data types
|
|
54
|
-
- Include error handling for network failures
|
|
55
|
-
|
|
56
|
-
**Local Storage Providers** (`src/examples/LocalStorage*Provider.ts`)
|
|
57
|
-
- Load configuration from local JSON files using `require()`
|
|
58
|
-
- Support both boolean and Feature data types
|
|
59
|
-
- Fallback to empty configuration on file load errors
|
|
60
|
-
|
|
61
|
-
**Evaluation Core (`src/evaluate.ts`)**
|
|
62
|
-
- `evaluate(feature, now)` is the single definition of the evaluation rules
|
|
63
|
-
- Providers call it instead of implementing the logic themselves; before this
|
|
64
|
-
existed, the feature providers each carried their own copy
|
|
65
|
-
- `Clock` is a `() => number`; providers take one and default to `systemClock`,
|
|
66
|
-
so tests and the conformance adapter can pin `now`
|
|
67
|
-
- `parseTimestamp` accepts only RFC 3339 with an offset and returns
|
|
68
|
-
`undefined` for anything else, warning rather than throwing
|
|
69
|
-
|
|
70
|
-
### Feature Data Model
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
type Feature = {
|
|
74
|
-
key: string; // Unique feature identifier
|
|
75
|
-
value: string; // Boolean value as string; only "true" is on
|
|
76
|
-
activeAt: string; // RFC 3339 with offset, or empty
|
|
77
|
-
disabledAt: string; // RFC 3339 with offset, or empty
|
|
78
|
-
tags?: string[];
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Features support time-based activation/deactivation logic evaluated at runtime.
|
|
83
|
-
The window is half-open: `now == activeAt` is on, `now == disabledAt` is off.
|
|
84
|
-
Dates without an offset are ignored, because languages disagree on how to read
|
|
85
|
-
them and a feature would otherwise flip at a different instant per port.
|
|
86
|
-
|
|
87
|
-
### Decorator Behavior
|
|
88
|
-
|
|
89
|
-
**Method Decoration:**
|
|
90
|
-
- Replaces method implementation based on feature state
|
|
91
|
-
- Supports fallback method when feature is disabled
|
|
92
|
-
- Returns empty function if no fallback provided
|
|
93
|
-
|
|
94
|
-
**Class Decoration:**
|
|
95
|
-
- Replaces entire class constructor and prototype
|
|
96
|
-
- Supports fallback class when feature is disabled
|
|
97
|
-
- Creates EmptyClass with stub methods if no fallback provided
|
|
98
|
-
- Preserves async method signatures in EmptyClass stubs
|
|
99
|
-
|
|
100
|
-
## Usage Patterns
|
|
101
|
-
|
|
102
|
-
### Initialization
|
|
103
|
-
```typescript
|
|
104
|
-
import { FeatureToggleBase } from "@tehw0lf/yaft";
|
|
105
|
-
import { LocalStorageFeatureProvider } from "./examples/LocalStorageFeatureProvider";
|
|
106
|
-
|
|
107
|
-
FeatureToggleBase.featureProvider = new LocalStorageFeatureProvider("./config.json");
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### Method Toggle
|
|
111
|
-
```typescript
|
|
112
|
-
@FeatureToggle("myFeature", fallbackMethod)
|
|
113
|
-
myMethod() {
|
|
114
|
-
// Implementation when feature is enabled
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### Class Toggle
|
|
119
|
-
```typescript
|
|
120
|
-
@FeatureToggle("myFeature", FallbackClass)
|
|
121
|
-
class MyClass {
|
|
122
|
-
// Implementation when feature is enabled
|
|
123
|
-
}
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
## Testing Strategy
|
|
127
|
-
|
|
128
|
-
Tests use LocalStorageFeatureProvider with test configuration files (`src/test/test-*.json`). The test suite demonstrates:
|
|
129
|
-
- Class decoration with fallback classes
|
|
130
|
-
- Method decoration with and without fallbacks
|
|
131
|
-
- Async method handling
|
|
132
|
-
- Provider configuration and feature evaluation
|
|
133
|
-
|
|
134
|
-
## Build Configuration
|
|
135
|
-
|
|
136
|
-
- **TypeScript**: CommonJS output targeting ES2022
|
|
137
|
-
- **Decorators**: Experimental decorators enabled for `@FeatureToggle` syntax
|
|
138
|
-
- **Output**: Compiled to `dist/yaft/` with declaration files
|
|
139
|
-
- **Package**: Strips devDependencies during build process
|
|
140
|
-
|
|
141
|
-
## Dependencies
|
|
142
|
-
|
|
143
|
-
- **Runtime**: `reflect-metadata` for decorator metadata
|
|
144
|
-
- **Development**: TypeScript, Jest, ts-jest for testing
|
|
145
|
-
- **Optional**: `axios` for API provider implementations
|
|
146
|
-
|
|
147
|
-
## Pre-commit Validation
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
npm test && npm run build
|
|
151
|
-
```
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Robert Weyres
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/conformance.lock
DELETED
package/jest.config.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/** @type {import('ts-jest').JestConfigWithTsJest} **/
|
|
2
|
-
module.exports = {
|
|
3
|
-
displayName: 'yaft',
|
|
4
|
-
setupFilesAfterEnv: [
|
|
5
|
-
'<rootDir>/src/test/test-setup.ts'
|
|
6
|
-
],
|
|
7
|
-
globals: {},
|
|
8
|
-
coverageDirectory: 'coverage/yaft',
|
|
9
|
-
testEnvironment: "node",
|
|
10
|
-
testMatch: [
|
|
11
|
-
'<rootDir>/src/**/*.spec.ts'
|
|
12
|
-
],
|
|
13
|
-
transform: {
|
|
14
|
-
"^.+\\.tsx?$": ["ts-jest", { tsconfig: "tsconfig.spec.json" }],
|
|
15
|
-
},
|
|
16
|
-
};
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# Fetches the YaFT conformance suite pinned in conformance.lock.
|
|
4
|
-
#
|
|
5
|
-
# Copy this into a port, next to a conformance.lock of the form:
|
|
6
|
-
#
|
|
7
|
-
# version=v1.0.0
|
|
8
|
-
# sha256=<checksum of cases.tar.gz>
|
|
9
|
-
#
|
|
10
|
-
# The checksum is not optional. A Git tag can be moved; verifying the asset is
|
|
11
|
-
# what stops a port's tests from changing without a diff.
|
|
12
|
-
#
|
|
13
|
-
# Usage: scripts/fetch-conformance.sh [target-dir] (default: src/test/conformance)
|
|
14
|
-
#
|
|
15
|
-
# Set the default to wherever the port's adapter reads the cases from. Ports
|
|
16
|
-
# differ -- yaft-ts uses src/test/conformance -- and a default that does not
|
|
17
|
-
# match leaves the next test run reporting missing cases.
|
|
18
|
-
|
|
19
|
-
set -euo pipefail
|
|
20
|
-
|
|
21
|
-
REPO="tehw0lf/yaft-conformance"
|
|
22
|
-
LOCK="${LOCK:-conformance.lock}"
|
|
23
|
-
TARGET="${1:-src/test/conformance}"
|
|
24
|
-
|
|
25
|
-
if [[ ! -f "$LOCK" ]]; then
|
|
26
|
-
echo "error: $LOCK not found; run from the port's root" >&2
|
|
27
|
-
exit 1
|
|
28
|
-
fi
|
|
29
|
-
|
|
30
|
-
version="$(grep -E '^version=' "$LOCK" | cut -d= -f2-)"
|
|
31
|
-
expected="$(grep -E '^sha256=' "$LOCK" | cut -d= -f2-)"
|
|
32
|
-
|
|
33
|
-
if [[ -z "$version" || -z "$expected" ]]; then
|
|
34
|
-
echo "error: $LOCK needs both version= and sha256=" >&2
|
|
35
|
-
exit 1
|
|
36
|
-
fi
|
|
37
|
-
|
|
38
|
-
tmp="$(mktemp -d)"
|
|
39
|
-
trap 'rm -rf "$tmp"' EXIT
|
|
40
|
-
|
|
41
|
-
url="https://github.com/${REPO}/releases/download/${version}/cases.tar.gz"
|
|
42
|
-
echo "fetching conformance suite ${version}"
|
|
43
|
-
curl --fail --location --silent --show-error --output "$tmp/cases.tar.gz" "$url"
|
|
44
|
-
|
|
45
|
-
# sha256sum is GNU coreutils and is not present on macOS; shasum ships with
|
|
46
|
-
# both. Preferring sha256sum keeps Linux CI on the faster binary.
|
|
47
|
-
if command -v sha256sum >/dev/null 2>&1; then
|
|
48
|
-
actual="$(sha256sum "$tmp/cases.tar.gz" | cut -d' ' -f1)"
|
|
49
|
-
elif command -v shasum >/dev/null 2>&1; then
|
|
50
|
-
actual="$(shasum -a 256 "$tmp/cases.tar.gz" | cut -d' ' -f1)"
|
|
51
|
-
else
|
|
52
|
-
echo "error: neither sha256sum nor shasum found; cannot verify the download" >&2
|
|
53
|
-
exit 1
|
|
54
|
-
fi
|
|
55
|
-
if [[ "$actual" != "$expected" ]]; then
|
|
56
|
-
echo "error: checksum mismatch for cases.tar.gz" >&2
|
|
57
|
-
echo " expected $expected" >&2
|
|
58
|
-
echo " actual $actual" >&2
|
|
59
|
-
echo "The tag may have been moved. Do not update the lock without reading the diff." >&2
|
|
60
|
-
exit 1
|
|
61
|
-
fi
|
|
62
|
-
|
|
63
|
-
rm -rf "$TARGET"
|
|
64
|
-
mkdir -p "$TARGET"
|
|
65
|
-
tar --extract --gzip --file "$tmp/cases.tar.gz" --directory "$TARGET"
|
|
66
|
-
|
|
67
|
-
echo "conformance suite ${version} unpacked into ${TARGET}"
|
package/src/FeatureToggle.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import "reflect-metadata";
|
|
2
|
-
|
|
3
|
-
class EmptyClass {
|
|
4
|
-
constructor() {}
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export type Feature = {
|
|
8
|
-
key: string;
|
|
9
|
-
value: string;
|
|
10
|
-
activeAt: string;
|
|
11
|
-
disabledAt: string;
|
|
12
|
-
tags?: string[];
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export interface FeatureProvider<T> {
|
|
16
|
-
apiUrl?: string;
|
|
17
|
-
baseUUID?: string;
|
|
18
|
-
data: Record<string, T>;
|
|
19
|
-
getCollectionHash?(configPathOrUrl: string): void;
|
|
20
|
-
getConfig(configPathOrUrl: string): void;
|
|
21
|
-
isEnabled(key: string): boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function FeatureToggle(key: string, fallback?: any) {
|
|
25
|
-
if (!FeatureToggleBase.featureProvider) {
|
|
26
|
-
throw new Error("FeatureToggleProvider not set");
|
|
27
|
-
}
|
|
28
|
-
return (
|
|
29
|
-
target: any,
|
|
30
|
-
propertyKey?: string,
|
|
31
|
-
descriptor?: PropertyDescriptor
|
|
32
|
-
) => {
|
|
33
|
-
if (propertyKey && descriptor) {
|
|
34
|
-
// Method
|
|
35
|
-
const originalMethod = descriptor.value;
|
|
36
|
-
|
|
37
|
-
// An async method must keep returning a promise when it is switched
|
|
38
|
-
// off, or `await` at the call site breaks on a plain undefined. The
|
|
39
|
-
// empty class shell below already makes this distinction; without it
|
|
40
|
-
// here, turning a feature off would throw inside unrelated code.
|
|
41
|
-
const isAsync =
|
|
42
|
-
originalMethod?.[Symbol.toStringTag] === "AsyncFunction";
|
|
43
|
-
|
|
44
|
-
descriptor.value = function (...args: any[]) {
|
|
45
|
-
const isEnabled = FeatureToggleBase.featureProvider.isEnabled(key);
|
|
46
|
-
if (isEnabled) {
|
|
47
|
-
return originalMethod.apply(this, args);
|
|
48
|
-
} else {
|
|
49
|
-
if (fallback !== undefined) {
|
|
50
|
-
const result = fallback.apply(this, args as []);
|
|
51
|
-
// A synchronous fallback on an async method would otherwise hand
|
|
52
|
-
// back a plain value, breaking the promise the signature
|
|
53
|
-
// advertises. Promise.resolve passes an existing promise through
|
|
54
|
-
// unchanged, so an async fallback is unaffected.
|
|
55
|
-
return isAsync ? Promise.resolve(result) : result;
|
|
56
|
-
}
|
|
57
|
-
return isAsync ? Promise.resolve() : undefined;
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
return descriptor;
|
|
61
|
-
} else {
|
|
62
|
-
// Class
|
|
63
|
-
const originalConstructor = target;
|
|
64
|
-
let newConstructor: any;
|
|
65
|
-
|
|
66
|
-
const isEnabled = FeatureToggleBase.featureProvider.isEnabled(key);
|
|
67
|
-
if (isEnabled) {
|
|
68
|
-
newConstructor = originalConstructor;
|
|
69
|
-
} else {
|
|
70
|
-
newConstructor = fallback !== undefined ? fallback : EmptyClass;
|
|
71
|
-
if (fallback) newConstructor.__proto__ = fallback.__proto__;
|
|
72
|
-
|
|
73
|
-
if (newConstructor === EmptyClass) {
|
|
74
|
-
Object.getOwnPropertyNames(originalConstructor.prototype).forEach(
|
|
75
|
-
(name: string) => {
|
|
76
|
-
if (name === "constructor") return;
|
|
77
|
-
if (typeof originalConstructor.prototype[name] === "function")
|
|
78
|
-
newConstructor.prototype[name] = () => {};
|
|
79
|
-
if (
|
|
80
|
-
originalConstructor.prototype[name][Symbol.toStringTag] ===
|
|
81
|
-
"AsyncFunction"
|
|
82
|
-
)
|
|
83
|
-
newConstructor.prototype[name] = async () => {};
|
|
84
|
-
}
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return newConstructor;
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export abstract class FeatureToggleBase {
|
|
94
|
-
static featureProvider: FeatureProvider<any>;
|
|
95
|
-
}
|