woolts / wool Goto Github PK
View Code? Open in Web Editor NEWA typescript ecosystem - package manager, decentralised registries, monorepo compiler
License: MIT License
A typescript ecosystem - package manager, decentralised registries, monorepo compiler
License: MIT License
Following on from #2, the wool/browser
package provide programs for creating websites and applications.
It uses the same underlying wool/ui
package as wool/terminal
and wool/native
to describe the layout of its elements.
All these programs allow the website / app to be built into the html, css and javascript artifacts.
wool run hello/world build
wool/browser
is a disappearing framework in a similar vein to Svelte. When compiled, the framework code that is required is embedded into your app and the rest is ditched.
It is a functional library, with a single store of state at the root and declarative view functions that describe how that state is presented.
A sandbox application can not communicate with the outside world but can react to user input.
interface Sandbox<Model, Msg> {
init: () => Model;
view: (model: Model) => UI.Layout;
update: (msg: Msg, model: Model) => Model;
}
import * as Browser from 'wool/browser';
import { layout, el, text } from 'wool/ui';
export default Browser.sandbox({
init: () => {},
view: (model) => layout([], el([], text`Hello, World!`)),
update: (msg, model) => model,
});
The syntax and functionality of
wool/ui
is a separate work-in-progress.
Events are declaratively added to interactions, such as pressing a button. The event is given a message that will be passed into the program's update function. This can then be used to update the model. The new model is then passed into the view function to create the new view.
Custom types are discussed in #3
import * as Browser from 'wool/browser';
import { Maybe, Type } from 'wool/core';
import { Events, Input, layout, column, el, button, text } from 'wool/ui';
interface Model {
greeting: string;
}
const SetGreeting = Type.custom('SetGreeting', Type.string);
const init = (): Model => ({
greeting: 'Hello',
});
const view = ({ greeting }: Model) => layout(
[],
column([], [
text`${greeting}, World!`,
Input.button([], {
onPress: Maybe.just(SetGreeting('Bonjour')),
label: el([], text`Change greeting`),
}),
]),
);
const update = (msg, model) => Type.matchOn(msg, {
[SetGreeting]: (greeting) => ({ greeting }),
});
export default Browser.sandbox({ init, view, update });
An application provides access to the document head and url. Though a routed application is probably more useful.
interface Application<Model, Msg> {
init: (url: Url, key?) => Tuple<Model, Cmd<Msg>>;
view: (model: Model) => Document<Msg>;
update: (msg: Msg, model: Model) => Tuple<Model, Cmd<Msg>>;
onUrlRequest: (req: UrlRequest) => Msg;
onUrlChange: (url: Url) => Msg;
}
interface Document<Msg> {
head: { title: String };
body: UI.Layout;
}
The routed application is a simplified application that handles routing for you. This is probably the ideal for most web apps.
interface RoutedApplication<Model, Msg> {
init: (url: Url, key?) => Tuple<Model, Cmd<Msg>>;
view: (model: Model) => Document<Msg>;
update: (msg: Msg, model: Model) => Tuple<Model, Cmd<Msg>>;
}
It will populate your model with the updated route whenever it changes.
const init = () => {};
const update = (msg, model) => model;
const view = (model) => ({
head: [],
body: layout(
[],
Type.matchOn(model[Browser.routeKey], {
'/': () => el([], text`Welcome`),
'/with-params': ({ greeting }) => el([], text`${greeting}`),
}),
),
});
export default Browser.routedApplication({ init, update, view });
Browser.routedApplication
const RouteRequest = Type.custom('RouteRequest', Browser.Type.routeRequest);
const RouteChange = Type.custom('RouteChange', Browser.Type.routeChange);
const routeKey = Type.custom('routeKey', Type.string);
Browser.routedApplication = (config) => Browser.application({
init: () => ({
...config.init(),
[routeKey]: new Route('/'),
}),
view: config.view,
update: (msg, model) => {
case newModel = Type.matchOn(msg, {
[RouteRequest]: (req) => ({...model, route: req }),
[RouteChange]: (url) => model,
});
return {
...config.update(msg, newModel),
[routeKey]: newModel[routeKey],
};
},
onUrlRequest: (req) => RouteRequest(req),
onUrlChange: (url) => RouteChange(url),
});
This design borrows heavily from elm/browser, which provides two further programs: element
and document
.
The element
program provides a way to embed an elm app within an existing webpage, which won't be needed with wool.
The document
program is the same as the application
but without the url routing. Is that useful? Or can users who want that just noop the url functions on application
?
At the root of Wool's philosophy are three concepts:
To that end, the wool/terminal
package will provide a way to create command line applications from simple single commands to git-style programs with sub-commands and curses / blessed style views.
All applications, be they cli or web-based, will be expected to export a default entry. This is much the same as a main
function in many common languages such as C and Python.
All these programs would automatically support --help
and --version
, e.g.:
wool run hello/world --version
> 1.0.0
And as per #1 they would require the user to give them permission before being able to perform dangerous actions such as calling functions from wool/fs
and similar.
A single action that takes no input from the outside world.
// ~/hello-world/wool.json
{
"name": "hello/world",
"entry": "index.ts"
}
// ~/hello-world/index.ts
import * as Terminal from 'wool/terminal';
export default Terminal.action(() => {
console.log('Hello, World!');
});
wool make ~/hello-world
wool run hello/world
> Hello, World!
A command that can be given arguments and flags.
import { command, arg, flag, required, alias, boolean, string } from 'wool/terminal';
export default Terminal.command({
args: [
arg('entry', required()),
],
flags: [
flag('clean', alias('c'), boolean(false)),
flag('outDir', alias('o'), string('./dist')),
],
action: (cmd) => {
if (cmd.clean) {
// ... clean the `cmd.outDir` ...
}
// ... do something with `cmd.entry` ...
},
});
wool run hello/world ./src/index.ts --outDir ./out
An application that can call off to many commands.
import { application, command } from 'wool/terminal';
export default application({
commands: {
one: command({ ... }),
two: command({ ... }),
}
})
wool run hello/world one ./src/index.ts --outDir ./out
A curses / blessed style view.
import * as Terminal from 'wool/terminal';
import { layout, el, text } from 'wool/ui';
export default Terminal.ui({
args: [...],
flags: [...],
init: (cmd) => {},
update: (msg, model) => {},
view: (model) => layout([], el([], text`Hello, World!`)),
})
wool run hello/world
The syntax and functionality of
wool/ui
is a separate work-in-progress.
Above, I've used a variable args syntax:
flag('clean');
flag('clean', alias('c'), boolean(false))
A strictly FP approach would look like:
boolean(alias(flag('clean'), 'c'), false)
But this is clearly a bit rubbish.
Another alternative would be chained functions, e.g.:
flag('clean').alias('c').boolean(false)
[TODO: placeholder, to be expanded)
Can a registry be a static file server?
a. Ownership transference key generated as a combination of package and an entered password
b. Publishing through ssh keys associated with the files on the server?
c. How would they upload? Don't want to allow file access to randoms.
If not static, a docker container.
a. What would be the upgrade path for a deployed registry? Without wiping out existing published packages.
Auth method?
a. Portier
b. Certificates
In light of the recent event-stream security issue, and the previous left-pad debacle, it seems sensible to open discussion on approaches to more secure methods of guaranteeing authorship and safe publishing & installation of packages.
note: "exists" refers to in the design, not code, wool is still very early pre-alpha
In its current design, wool helps surface indirect / transient packages. This helps developers be fully aware of the quantity of dependencies in their project and whose code is running.
wool add hello/world
To add hello/world I would like to install the following packages:
hello/world 1.2.0 1.0.0 <= v < 2.0.0
hello/other 2.0.0 2.0.0 <= v < 3.0.0
Should I install these packages? [Y/n]
{
"dependencies": {
"direct": {
"hello/world": "1.0.0 <= v < 2.0.0"
},
"indirect": {
"hello/other": "2.0.0 <= v < 3.0.0"
}
}
}
It also only allows a single entry point into packages with an automatically semantically versioned interface.
When creating a project, the developer must choose which package registries to point at (defaulting to just registry.wool.org
).
{
"registries": ["registry.wool.org"]
}
All registries must be accessible over https.
When adding a package, its registry is stored in the wool.lock
, e.g.
{
"hello/world": {
"version": "1.2.0",
"constraint": "1.0.0 <= v < 2.0.0",
"registry": "registry.wool.org"
}
}
Most packages do not need direct access to the filesystem, so by default they should not be allowed to read or write anywhere.
Most packages also do not need network access, so by default they should also not be allowed to send network requests.
Packages should be provided opt-in permissions to let them perform certain, risky actions.
When running wool make .
, wool would warn them that a package does not have the permissions it requires:
❖ PERMISSIONS REQUIRED --------------------------------------------- hello/world
The package hello/world requires the following permissions:
"read": true
"write: ["./path/to/folder"]
Do you wish to grant these permissions to hello/world? [y/N]
This would then update their wool.json
and re-attempt the compilation.
{
"dependencies": {
"direct": {
- "hello/world": "1.0.0 <= v < 2.0.0"
+ "hello/world": {
+ "constraint": "1.0.0 <= v < 2.0.0",
+ "permissions": {
+ "read": true,
+ "write": ["./path/to/folder"]
+ }
+ }
}
}
}
For example, to restrict file access this can be acheived through the wool loader by preventing installed packages from using the fs
package directly.
Instead import via wool/fs
(with the loader explicitly only allowing the wool
namespace direct access to node in-built packages).
-import * as fs from 'fs';
+import * as fs from 'wool/fs';
2-factor authentication should be mandatory for all publishing, even to private registries.
It should be possible for maintainers to archive packages and put them in a read-only
mode.
wool archive .
Both archiving and unarchiving a package would require 2-factor authentication.
Developers adding archived projects will be warned:
❖ PACKAGE ARCHIVED ----------------------------------------------- hello/world
The package hello/world has been archived and is no longer being maintained.
It may present a security risk for your project if there are un-patched
vulnerabilities.
Do you wish to continue using hello/world? [y/N]
{
"dependencies": {
"direct": {
- "hello/world": "1.0.0 <= v < 2.0.0"
+ "hello/world": {
+ "constraint": "1.0.0 <= v < 2.0.0",
+ "allowArchived": true
+ }
}
}
}
It should be feasible to transfer ownership of a project that the maintainer no longer wishes to support.
This would involve the owner pushing a "transference key" to the registry, this would immediately lock the package and prevent any new publications. They would then share the key with the intended new owner, who would then use it to unlock and take control of the package.
Only one account should be able to publish a package at any given time.
Transferal of ownership would enforce the next publication to be a major version update bump.
If a developer updates their constraint to the next major version they will be warned about the ownership change.
❖ PACKAGE OWNER CHANGED --------------------------------------- hello/world
The package `hello/world` has transferred ownership from:
[email protected]
to:
[email protected]
Before updating from 1.2.0 to 2.0.0 you should check the new package owner
is trustworthy.
Do you wish to continue? [y/N]
When compiling, check the imported package code against their checksums stored in the wool.lock
.
{
"hello/world": {
"version": "1.2.0",
"constraint": "1.0.0 <= v < 2.0.0",
"registry": "registry.wool.org",
"checksum": "123456"
}
}
This could be generated by running a concatenated toString()
on the exported functions & values. If another package mutates the function it will return a different toString()
value and fail the checksum validation.
e.g.
import hello from 'hello/world'
let originalGreet = hello.greet;
hello.greet = async (message) => {
await mineSomeBitcoin();
return hello.greet(message);
};
One of the main issues around a decentralised approach to package registries is namespace squatting and package name clashes.
Even from non-malicious actors this would be confusing and annoying.
If a package name exists on multiple registries, the user will be asked to confirm which registry they wish to install from:
❖ PACKAGE NAME CLASH --------------------------------------- hello/world
The package `hello/world` exists on more than one registry:
registry.wool.org
registry.wool-community.net
Which registry should I download this package from? # multiple choice input
{
"registries": ["registry.wool.org"]
"dependencies": {
"direct": {
- "hello/world": "1.0.0 <= v < 2.0.0"
+ "hello/world": {
+ "constraint": "1.0.0 <= v < 2.0.0",
+ "registry": "registry.wool.org"
+ }
}
}
}
Is this enough? Is there any sensible way to prevent name clashing between decentralised registries?
What about namespace squatting?
Say the company hello registers their namespace on registry.wool.org
, and publishes hello/world
.
Then on another registry, for example registry.wool-community.net
, an unaffiliated user registers hello
. What recourse can the company hello take? Can the user mimic the company and freely cause confusion?
Should there be a single central authority for public namespace registration? Ideally not.
Perhaps allow whole namespaces to be tied to registries, instead of on a per-package basis.
{
"registries": [
"registry.wool.org",
+ {
+ "namespace": "hello",
+ "registry": "registry.hello.com"
+ }
],
...
}
And importantly, how can wool assuage non-developer stakeholders of the risks?
Should publishing be tied to the git repository? The primary downside is it would force package authors to use git, however this is by far the most dominant vcs used. And security is more important than convenience.
This would allow commit verification as a requirement.
It could be an opt-in for registries, i.e. a registry could chose to force all users to verify their commits.
There are probably numerous other issues and attack vectors possible.
Custom types are also known as "union types", "tagged unions" and "algebraic data types".
While typescript provides a way to have compile-time union types, they are no longer available at runtime, which means they can't be used for active behaviours (such as messages in wool/browser
).
import { Type } from 'wool/core';
const Increment = Type.custom('Increment');
const Decrement = Type.custom('Decrement');
let value = 0;
const msg = Increment();
Type.matchOn(msg, {
[Increment]() { value = value + 1; },
[Decrement]() { value = value - 1; },
});
console.log(value);
// 1
import { Type } from 'wool/core';
const SetValue = Type.custom('SetValue', Type.int);
const msg = SetValue(42);
Type.matchOn(msg, {
// ...
[SetValue](newValue) { value = newValue; },
});
console.log(value);
// 42
import { String, Type } from 'wool/core';
const SetGreeting = Type.custom('SetGreeting', Type.string, Type.int)
let value = 'Hi';
const msg = SetGreeting('Hello', 3);
Type.matchOn(msg, {
[SetGreeting](greeting, times) {
value = String.repeat(greeting, times);
},
});
console.log(value);
// "HelloHelloHello"
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.