Skip to content

Commit 74f15dd

Browse files
committed
feat: Initial implementation
1 parent 48c541d commit 74f15dd

7 files changed

Lines changed: 180 additions & 134 deletions

File tree

README.md

Lines changed: 1 addition & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1 @@
1-
# A template repository for TypeScript and Components.js
2-
3-
This repository can help you get started with a new TypeScript project
4-
which generates Components.js components for its classes.
5-
6-
It has support for testing, linting, pre-commit checks, CI, releasing new versions, and keeping dependencies up to date.
7-
Each of these will be explained below.
8-
9-
## DO THIS FIRST
10-
11-
* Add a `name` field to the `package.json`
12-
* In the `package.json`, replace `$PACKAGE_NAME` with the name chosen in the previous step.
13-
* Add a `repository` to the `package.json`. This is not strictly required,
14-
but otherwise the generated `CHANGELOG.md` will not contain links.
15-
* (Optional) Add a `description` to the `package.json`.
16-
* (Optional) Add the `-r` parameter to the `build:components` script.
17-
This determines the prefix that will be used for paths generated by the Components.js generator,
18-
otherwise this is determined automatically based on your package name.
19-
* (Optional) Replace this README with your own and add some fancy [badges](https://shields.io/badges/npm-version).
20-
21-
## Testing
22-
23-
The repo uses `jest` for testing.
24-
Tests are expected in the following format: `/test/(unit|integration)/.*\.test\.ts$`,
25-
so your tests need to end on `.test.ts` and should be in (a subfolder of) `test/unit` or `test/integration`.
26-
This can be changed in `jest.config.js`.
27-
28-
Note that the `test` folder has its own tsconfig.json.
29-
This is necessary to make sure those files are not built but are covered by the linter.
30-
31-
Coverage will be generated when testing,
32-
but no coverage restrictions are set.
33-
If you want to ensure 100% code coverage,
34-
add the following to your `jest.config.js`:
35-
36-
```text
37-
coverageThreshold: {
38-
'./src': {
39-
branches: 100,
40-
functions: 100,
41-
lines: 100,
42-
statements: 100,
43-
}
44-
```
45-
46-
## Linting
47-
48-
Linting is done using [opinionated-eslint-config](https://github.com/joachimvh/opinionated-eslint-config).
49-
See the documentation there if you want to make changes.
50-
51-
Besides that, there is also markdown linting which is defined by the settings in `.markdownlint-cli2.cjs`.
52-
The `.github` folder has its own settings for markdown linting to prevent issues with the templates discussed below.
53-
54-
## Committing
55-
56-
Before committing, the build, lint, and test scripts will be run.
57-
58-
Commit messages are expected to be in the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) format.
59-
Unfortunately this can only be checked automatically after the pre-commit checks,
60-
so make sure you have it right the first time.
61-
The rules for this are specified in `commitlint.config.js`.
62-
63-
## Continuous Integration
64-
65-
`.github/workflows/test.yml` contains a GitHub CI file that will trigger linting and testing for PRs and new commits.
66-
67-
## Releasing
68-
69-
To release a new version, run `npm run release -- -r major/minor/patch`,
70-
with `major`, `minor`, or `patch` chosen based on [Semantic Versioning](https://semver.org/).
71-
This will create, or update, `CHANGELOG.md` based on the commits since the previous release,
72-
and tag the release.
73-
In the case of a major release,
74-
it will also update the context entries of all Components.js configurations in the `config` folder,
75-
and update the necessary Components.js fields in the `package.json`.
76-
After this you still have to manually push the commit and tag to GitHub.
77-
78-
The format of the changelog is defined in `.versionrc.json`.
79-
80-
You can do an alpha release using, for example, `npm run release -- -r major --prerelease alpha`.
81-
82-
You can do a dry-run to see the results by adding the `--dry-run` option.
83-
84-
If you want to update the contexts of Components.js configurations in other folders than `config`,
85-
you will need to add those in `scripts/upgradeConfig.ts`.
86-
87-
## GitHub
88-
89-
Templates are included for PRs and new issues.
90-
91-
If you want to add issue templates, this can be done in `.github/ISSUE_TEMPLATE`.
92-
93-
If you want users to only be able to use those templates,
94-
create `.github/ISSUE_TEMPLATE/config.yml` and add `blank_issues_enabled: false`.
95-
96-
If you enable discussions, you can direct uses there when creating a new issue
97-
by adding the following to the same `.github/ISSUE_TEMPLATE/config.yml`:
98-
99-
```yaml
100-
contact_links:
101-
- name: ❓ Question
102-
url: https://github.com/$MY_ORG/$MY_REPO/discussions/new/choose
103-
about: A question or discussion about the project
104-
```
105-
106-
## Dependabot
107-
108-
A Dependabot config is included in `.github/dependabot.yml`
109-
to get notified when new major releases of libraries get released.
110-
You still have to manually add Dependabot to your repository
111-
as described [here](https://docs.github.com/en/code-security/getting-started/dependabot-quickstart-guide).
112-
If you also want to be notified of minor/patch releases you will have to update the configuration.
113-
114-
## Removing Components.js
115-
116-
This template is made for repositories that want to support Components.js,
117-
but in case you do not want to do that, this can be removed with the following steps:
118-
119-
* In `package.json`
120-
* Remove all fields starting with `lsd:`.
121-
* Change the `build` script to only perform the steps of `build:ts` and remove `build:components`.
122-
* Remove the `componentsjs-generator` dependency.
123-
* In the `commit-and-tag-version` settings, remove the `postbump` step.
124-
* Remove the `config/dummy.json`, `scripts/upgradeConfig.ts`, and `.componentsignore` files.
1+
# RDF Vocabulary

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ const opinionated = require('opinionated-eslint-config');
22

33
module.exports = opinionated({
44
typescript: {
5-
tsconfigPath: [ './tsconfig.json', './scripts/tsconfig.json', './test/tsconfig.json' ],
5+
tsconfigPath: [ './tsconfig.json', './test/tsconfig.json' ],
66
},
77
});

package-lock.json

Lines changed: 12 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
"release": "commit-and-tag-version",
2121
"test": "jest"
2222
},
23+
"dependencies": {
24+
"@rdfjs/types": "^1.1.0"
25+
},
2326
"devDependencies": {
2427
"@commitlint/cli": "^19.4.0",
2528
"@commitlint/config-conventional": "^19.2.2",

src/index.ts

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,113 @@
1-
// Export everything that needs to be built by Components.js here
1+
import type { NamedNode, Term } from '@rdfjs/types';
2+
3+
class SimpleNamedNode implements NamedNode {
4+
public readonly termType = 'NamedNode';
5+
6+
public constructor(public readonly value: string) {}
7+
8+
public equals(other: Term | null | undefined): boolean {
9+
return Boolean(other) && other!.termType === this.termType && other!.value === this.value;
10+
}
11+
}
12+
13+
/**
14+
* A `Record` in which each value is a concatenation of the baseUrl and its key.
15+
*/
16+
type ExpandedRecord<TBase extends string, TLocal extends string> = {[K in TLocal]: `${TBase}${K}` };
17+
18+
/**
19+
* Has a base URL as `namespace` value and each key has as value the concatenation with that base URL.
20+
*/
21+
type ValueVocabulary<TBase extends string, TLocal extends string> =
22+
{ namespace: TBase } & ExpandedRecord<TBase, TLocal>;
23+
/**
24+
* A {@link ValueVocabulary} where the URI values are {@link NamedNode}s.
25+
*/
26+
type TermVocabulary<T> = T extends ValueVocabulary<string, string> ? {[K in keyof T]: NamedNode<T[K]> } : never;
27+
28+
/**
29+
* Contains a namespace and keys linking to the entries in this namespace.
30+
* The `terms` field contains the same values but as {@link NamedNode} instead of string.
31+
*/
32+
export type Vocabulary<TBase extends string, TKey extends string> =
33+
ValueVocabulary<TBase, TKey> & { terms: TermVocabulary<ValueVocabulary<TBase, TKey>> };
34+
35+
/**
36+
* A {@link Vocabulary} where all the non-namespace fields are of unknown value.
37+
* This is a fallback in case {@link createVocabulary} gets called with a non-strict string array.
38+
*/
39+
export type PartialVocabulary<TBase extends string> =
40+
{ namespace: TBase } &
41+
Partial<Record<string, string>> &
42+
{ terms: { namespace: NamedNode<TBase> } & Partial<Record<string, NamedNode>> };
43+
44+
/**
45+
* A local name of a {@link Vocabulary}.
46+
*/
47+
export type VocabularyLocal<T> = T extends Vocabulary<string, infer TKey> ? TKey : never;
48+
/**
49+
* A URI string entry of a {@link Vocabulary}.
50+
*/
51+
export type VocabularyValue<T> = T extends Vocabulary<string, infer TKey> ? T[TKey] : never;
52+
/**
53+
* A {@link NamedNode} entry of a {@link Vocabulary}.
54+
*/
55+
export type VocabularyTerm<T> = T extends Vocabulary<string, infer TKey> ? T['terms'][TKey] : never;
56+
57+
/**
58+
* Creates a {@link ValueVocabulary} with the given `baseUri` as namespace and all `localNames` as entries.
59+
*/
60+
function createValueVocabulary<TBase extends string, TLocal extends string>(baseUri: TBase, localNames: TLocal[]):
61+
ValueVocabulary<TBase, TLocal> {
62+
const expanded: Partial<ExpandedRecord<TBase, TLocal>> = {};
63+
// Expose the listed local names as properties
64+
for (const localName of localNames) {
65+
expanded[localName] = `${baseUri}${localName}`;
66+
}
67+
return {
68+
namespace: baseUri,
69+
...expanded as ExpandedRecord<TBase, TLocal>,
70+
};
71+
}
72+
73+
/**
74+
* Creates a {@link TermVocabulary} based on the provided {@link ValueVocabulary}.
75+
*/
76+
function createTermVocabulary<TBase extends string, TLocal extends string>(values: ValueVocabulary<TBase, TLocal>):
77+
TermVocabulary<ValueVocabulary<TBase, TLocal>> {
78+
// Need to cast since `fromEntries` typings aren't strict enough
79+
return Object.fromEntries(
80+
Object.entries(values).map(([ key, value ]): [string, NamedNode] => [ key, new SimpleNamedNode(value) ]),
81+
) as TermVocabulary<ValueVocabulary<TBase, TLocal>>;
82+
}
83+
84+
/**
85+
* Creates a {@link Vocabulary} with the given `baseUri` as namespace and all `localNames` as entries.
86+
* The values are the local names expanded from the given base URI as strings.
87+
* The `terms` field contains all the same values but as {@link NamedNode} instead.
88+
*/
89+
export function createVocabulary<TBase extends string, TLocal extends string>(baseUri: TBase, ...localNames: TLocal[]):
90+
string extends TLocal ? PartialVocabulary<TBase> : Vocabulary<TBase, TLocal> {
91+
const values = createValueVocabulary(baseUri, localNames);
92+
return {
93+
...values,
94+
terms: createTermVocabulary(values),
95+
};
96+
}
97+
98+
/**
99+
* Creates a new {@link Vocabulary} that extends an existing one by adding new local names.
100+
*
101+
* @param vocabulary - The {@link Vocabulary} to extend.
102+
* @param newNames - The new local names that need to be added.
103+
*/
104+
export function extendVocabulary<TBase extends string, TLocal extends string, TNew extends string>(
105+
vocabulary: Vocabulary<TBase, TLocal>,
106+
...newNames: TNew[]
107+
):
108+
ReturnType<typeof createVocabulary<TBase, TLocal | TNew>> {
109+
const localNames = Object.keys(vocabulary)
110+
.filter((key): boolean => key !== 'terms' && key !== 'namespace') as TLocal[];
111+
const allNames = [ ...localNames, ...newNames ];
112+
return createVocabulary(vocabulary.namespace, ...allNames);
113+
}

test/unit/MyClass.test.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

test/unit/index.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { createVocabulary, extendVocabulary } from '../../src/index';
2+
3+
describe('Vocabularies', (): void => {
4+
const vocabulary = createVocabulary('http://www.w3.org/ns/ldp#', 'contains', 'Container');
5+
6+
describe('createVocabulary', (): void => {
7+
it('contains its own URI.', (): void => {
8+
expect(vocabulary.namespace).toBe('http://www.w3.org/ns/ldp#');
9+
});
10+
11+
it('contains its own URI as a term.', (): void => {
12+
expect(vocabulary.terms.namespace.value).toBe('http://www.w3.org/ns/ldp#');
13+
expect(vocabulary.terms.namespace.equals(vocabulary.terms.namespace)).toBe(true);
14+
});
15+
16+
it('exposes the defined URIs.', (): void => {
17+
expect(vocabulary.contains).toBe('http://www.w3.org/ns/ldp#contains');
18+
expect(vocabulary.Container).toBe('http://www.w3.org/ns/ldp#Container');
19+
});
20+
21+
it('exposes the defined URIs as terms.', (): void => {
22+
expect(vocabulary.terms.contains.value).toBe('http://www.w3.org/ns/ldp#contains');
23+
expect(vocabulary.terms.Container.value).toBe('http://www.w3.org/ns/ldp#Container');
24+
});
25+
});
26+
27+
describe('extendVocabulary', (): void => {
28+
const extended = extendVocabulary(vocabulary, 'extended', 'extra');
29+
30+
it('still contains all the original values.', async(): Promise<void> => {
31+
expect(extended.namespace).toBe('http://www.w3.org/ns/ldp#');
32+
expect(extended.terms.namespace.value).toBe('http://www.w3.org/ns/ldp#');
33+
expect(extended.contains).toBe('http://www.w3.org/ns/ldp#contains');
34+
expect(extended.Container).toBe('http://www.w3.org/ns/ldp#Container');
35+
expect(extended.terms.contains.value).toBe('http://www.w3.org/ns/ldp#contains');
36+
expect(extended.terms.Container.value).toBe('http://www.w3.org/ns/ldp#Container');
37+
});
38+
39+
it('contains the new values.', async(): Promise<void> => {
40+
expect(extended.extended).toBe('http://www.w3.org/ns/ldp#extended');
41+
expect(extended.extra).toBe('http://www.w3.org/ns/ldp#extra');
42+
expect(extended.terms.extended.value).toBe('http://www.w3.org/ns/ldp#extended');
43+
expect(extended.terms.extra.value).toBe('http://www.w3.org/ns/ldp#extra');
44+
});
45+
46+
it('does not modify the original vocabulary.', async(): Promise<void> => {
47+
expect((vocabulary as any).extended).toBeUndefined();
48+
});
49+
});
50+
});

0 commit comments

Comments
 (0)