Initial commit

This commit is contained in:
ronreg-ribdev 2020-05-10 04:24:46 -07:00
commit 392ba83009
33 changed files with 7497 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

5
.urbitrc Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
URBIT_PIERS: [
"%URBITPIER%",
]
};

50
README.md Normal file
View File

@ -0,0 +1,50 @@
Get started building a simple application for Landscape on your [Urbit](http://urbit.org) ship with a few commands.
This tool is experimental and primarily used internally to develop front-end applications. While Tlon does not officially support this tool, you can always get general programming help for Urbit in the `~dopzod/urbit-help` chat.
## Installation
This repository is available as a template; to immediately generate your application's repository you can click [here](https://github.com/urbit/create-landscape-app/generate). Clone the generated repository, `npm install` and then run `npm start` to get started (you can also directly clone this repository, if you wish!).
In order to run your application on your ship, you will need Urbit v.0.10.0 or higher. On your Urbit ship, if you haven't already, mount your pier to Unix with `|mount %`.
## Using
Once you're up and running, your tile lives in `tile/tile.js`, which uses [React](https://reactjs.org) to render itself -- you'll want a basic foothold with it first. When you make changes, the `urbit` directory will update with the compiled application and, if you're running `npm run serve`, it will automatically copy itself to your Urbit ship when you save your changes (more information on that below).
### `npm start`
This runs the wizard. Give it an application name and the location of your Urbit ship's desk and it will customise the files so your new application will run on your ship.
### `npm run build`
This builds your application and copies it into your Urbit ship's desk. In your Urbit (v.0.8.0 or higher) `|commit %home` (or `%your-desk-name`) to synchronise your changes.
If this is the first time you're running your application on your Urbit ship, don't forget to `|start %yourapp`.
### `npm run serve`
Builds the application and copies it into your Urbit ship's desk, watching for changes. In your Urbit (v.0.8.0 or higher) `|commit %home` (or `%your-desk-name`) to synchronise your changes.
## FAQ
### What is a "tile" vs. "full" app?
When you run `npm run start`, the wizard will ask you to specify which you want:
- **tile**: A tile that exists on the Landscape launch screen. A pre-existing example is the "Weather" tile or the "Clock" tile in Landscape.
- **full**: A tile that links to a full-screen application: this means that you will work in both `tile.js` (for the tile interface) and `root.js` (and beyond) in the `src` folder.
No matter which option you specify, the wizard will customise the Hoon boilerplate for you and provide a basic example accordingly.
### How can I ensure my app fits Landscape design?
Landscape makes use of the [Indigo](https://urbit.github.io/indigo-react/) CSS framework. The template tile and full application both make use of it as an example for you to get going fast.
### What if I want to communicate with my ship / provide more functionality besides a front-end?
By default, your app will provide an example of passing state from ship to front-end with the `peer-[yourappname]tile` arm in the app's .hoon file -- in this case, just sending your ship's name as a data prop. The code is well-commented if you don't want to pass state, or if you want to know how to pass almost anything else from your ship to the Landscape interface.
In order to do anything substantial, of course, you'll want to know [Hoon](https://urbit.org/docs/tutorials/hoon/). If this is intimidating, don't panic: `create-landscape-app` is a fantastic way to start learning by leveraging your strengths. This repository is intended to be a boilerplate for rapid front-end development; it's also a gradual, incremental introduction to Hoon for web developers by allowing for rapid prototyping and experimentation with the Landscape interface.
Happy hacking!

164
full/gulpfile.js Normal file
View File

@ -0,0 +1,164 @@
var gulp = require('gulp');
var cssimport = require('gulp-cssimport');
var rollup = require('gulp-better-rollup');
var cssnano = require('cssnano');
var postcss = require('gulp-postcss');
var sucrase = require('@sucrase/gulp-plugin');
var minify = require('gulp-minify');
var resolve = require('rollup-plugin-node-resolve');
var commonjs = require('rollup-plugin-commonjs');
var rootImport = require('rollup-plugin-root-import');
var globals = require('rollup-plugin-node-globals');
/***
Main config options
***/
var urbitrc = require('./.urbitrc');
/***
End main config options
***/
gulp.task('css-bundle', function() {
let plugins = [
cssnano()
];
return gulp
.src('src/index.css')
.pipe(cssimport())
.pipe(postcss(plugins))
.pipe(gulp.dest('./urbit/app/%APPNAME%/css'));
});
gulp.task('jsx-transform', function(cb) {
return gulp.src('src/**/*.js')
.pipe(sucrase({
transforms: ['jsx']
}))
.pipe(gulp.dest('dist'));
});
gulp.task('tile-jsx-transform', function(cb) {
return gulp.src('tile/**/*.js')
.pipe(sucrase({
transforms: ['jsx']
}))
.pipe(gulp.dest('dist'));
});
gulp.task('js-imports', function(cb) {
return gulp.src('dist/index.js')
.pipe(rollup({
plugins: [
commonjs({
namedExports: {
'node_modules/react/index.js': [ 'Component' ],
'node_modules/react-is/index.js': [ 'isValidElementType' ],
}
}),
rootImport({
root: `${__dirname}/dist/js`,
useEntry: 'prepend',
extensions: '.js'
}),
globals(),
resolve()
]
}, 'umd'))
.on('error', function(e){
console.log(e);
cb();
})
.pipe(gulp.dest('./urbit/app/%APPNAME%/js/'))
.on('end', cb);
});
gulp.task('tile-js-imports', function(cb) {
return gulp.src('dist/tile.js')
.pipe(rollup({
plugins: [
commonjs({
namedExports: {
'node_modules/react/index.js': [ 'Component' ],
}
}),
rootImport({
root: `${__dirname}/dist/js`,
useEntry: 'prepend',
extensions: '.js'
}),
globals(),
resolve()
]
}, 'umd'))
.on('error', function(e){
console.log(e);
cb();
})
.pipe(gulp.dest('./urbit/app/%APPNAME%/js/'))
.on('end', cb);
});
gulp.task('js-minify', function () {
return gulp.src('./urbit/app/%APPNAME%/js/index.js')
.pipe(minify())
.pipe(gulp.dest('./urbit/app/%APPNAME%/js/'));
});
gulp.task('tile-js-minify', function () {
return gulp.src('./urbit/app/%APPNAME%/js/tile.js')
.pipe(minify())
.pipe(gulp.dest('./urbit/app/%APPNAME%/js/'));
});
gulp.task('urbit-copy', function () {
let ret = gulp.src('urbit/**/*');
urbitrc.URBIT_PIERS.forEach(function(pier) {
ret = ret.pipe(gulp.dest(pier));
});
return ret;
});
gulp.task('js-bundle-dev', gulp.series('jsx-transform', 'js-imports'));
gulp.task('tile-js-bundle-dev', gulp.series('tile-jsx-transform', 'tile-js-imports'));
gulp.task('js-bundle-prod', gulp.series('jsx-transform', 'js-imports', 'js-minify'))
gulp.task('tile-js-bundle-prod',
gulp.series('tile-jsx-transform', 'tile-js-imports', 'tile-js-minify'));
gulp.task('bundle-dev',
gulp.series(
gulp.parallel(
'css-bundle',
'js-bundle-dev',
'tile-js-bundle-dev'
),
'urbit-copy'
)
);
gulp.task('bundle-prod',
gulp.series(
gulp.parallel(
'css-bundle',
'js-bundle-prod',
'tile-js-bundle-prod',
),
'urbit-copy'
)
);
gulp.task('default', gulp.series('bundle-dev'));
gulp.task('watch', gulp.series('default', function() {
gulp.watch('tile/**/*.js', gulp.parallel('tile-js-bundle-dev'));
gulp.watch('src/**/*.js', gulp.parallel('js-bundle-dev'));
gulp.watch('src/**/*.css', gulp.parallel('css-bundle'));
gulp.watch('urbit/**/*', gulp.parallel('urbit-copy'));
}));

158
full/src/css/custom.css Normal file
View File

@ -0,0 +1,158 @@
p, h1, h2, h3, h4, h5, h6, a, input, textarea, button {
margin-block-end: unset;
margin-block-start: unset;
-webkit-margin-before: unset;
-webkit-margin-after: unset;
font-family: Inter, sans-serif;
}
a {
color: #000;
text-decoration: none;
}
textarea, select, input, button {
outline: none;
-webkit-appearance: none;
border: none;
background-color: #fff;
}
.body-regular {
font-size: 16px;
line-height: 24px;
font-weight: 600;
}
.body-large {
font-size: 20px;
line-height: 24px;
}
.label-regular {
font-size: 14px;
line-height: 24px;
}
.label-small-mono {
font-size: 12px;
line-height: 24px;
font-family: "Source Code Pro", monospace;
}
.body-regular-400 {
font-size: 16px;
line-height: 24px;
font-weight: 400;
}
.plus-font {
font-size: 48px;
line-height: 24px;
}
.btn-font {
font-size: 14px;
line-height: 16px;
font-weight: 600;
}
.mono {
font-family: "Source Code Pro", monospace;
}
.inter {
font-family: Inter, sans-serif;
}
.mix-blend-diff {
mix-blend-mode: difference;
}
/* dark */
@media (prefers-color-scheme: dark) {
body {
background-color: #333;
}
.bg-black-d {
background-color: black;
}
.white-d {
color: white;
}
.gray1-d {
color: #4d4d4d;
}
.gray2-d {
color: #7f7f7f;
}
.gray3-d {
color: #b1b2b3;
}
.gray4-d {
color: #e6e6e6;
}
.bg-gray0-d {
background-color: #333;
}
.bg-gray1-d {
background-color: #4d4d4d;
}
.b--gray0-d {
border-color: #333;
}
.b--gray1-d {
border-color: #4d4d4d;
}
.b--gray2-d {
border-color: #7f7f7f;
}
.b--white-d {
border-color: #fff;
}
.bb-d {
border-bottom-width: 1px;
border-bottom-style: solid;
}
.invert-d {
filter: invert(1);
}
.o-80-d {
opacity: .8;
}
.focus-b--white-d:focus {
border-color: #fff;
}
a {
color: #fff;
}
.hover-bg-gray1-d:hover {
color: #4d4d4d;
}
}
/* responsive */
@media all and (max-width: 34.375em) {
.h-100-minus-40-s {
height: calc(100% - 40px);
}
}
@media all and (min-width: 34.375em) and (max-width: 46.875em) {
.h-100-minus-40-m {
height: calc(100% - 40px);
}
}
@media all and (min-width: 46.875em) and (max-width: 60em) {
.h-100-minus-40-l {
height: calc(100% - 40px);
}
}
@media all and (min-width: 60em) {
.h-100-minus-40-xl {
height: calc(100% - 40px);
}
}

63
full/src/css/fonts.css Normal file
View File

@ -0,0 +1,63 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url("https://media.urbit.org/fonts/Inter-Regular.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
src: url("https://media.urbit.org/fonts/Inter-Italic.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url("https://media.urbit.org/fonts/Inter-Bold.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
src: url("https://media.urbit.org/fonts/Inter-BoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-extralight.woff");
font-weight: 200;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-light.woff");
font-weight: 300;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
font-weight: 400;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-medium.woff");
font-weight: 500;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-semibold.woff");
font-weight: 600;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-bold.woff");
font-weight: 700;
}

File diff suppressed because one or more lines are too long

4
full/src/index.css Normal file
View File

@ -0,0 +1,4 @@
@import 'css/indigo-static.css';
@import 'css/fonts.css';
@import 'css/custom.css';

16
full/src/index.js Normal file
View File

@ -0,0 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Root } from '/components/root';
import { api } from '/api';
import { store } from '/store';
import { subscription } from "/subscription";
api.setAuthTokens({
ship: window.ship
});
subscription.start();
ReactDOM.render((
<Root />
), document.querySelectorAll("#root")[0]);

49
full/src/js/api.js Normal file
View File

@ -0,0 +1,49 @@
import React from 'react';
import ReactDOM from 'react-dom';
import _ from 'lodash';
class UrbitApi {
setAuthTokens(authTokens) {
this.authTokens = authTokens;
this.bindPaths = [];
}
bind(path, method, ship = this.authTokens.ship, appl = "%APPNAME%", success, fail) {
this.bindPaths = _.uniq([...this.bindPaths, path]);
window.subscriptionId = window.urb.subscribe(ship, appl, path,
(err) => {
fail(err);
},
(event) => {
success({
data: event,
from: {
ship,
path
}
});
},
(err) => {
fail(err);
});
}
%APPNAME%(data) {
this.action("%APPNAME%", "json", data);
}
action(appl, mark, data) {
return new Promise((resolve, reject) => {
window.urb.poke(ship, appl, mark, data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
});
});
}
}
export let api = new UrbitApi();
window.api = api;

View File

@ -0,0 +1,48 @@
import React, { Component } from "react";
import { cite } from '../../lib/util';
import { IconHome } from "/components/lib/icons/icon-home";
import { Sigil } from "/components/lib/icons/sigil";
export class HeaderBar extends Component {
render() {
let title = document.title === "Home" ? "" : document.title;
return (
<div
className={
"bg-white bg-gray0-d w-100 justify-between relative tc pt3 db"
}
style={{ height: 40 }}>
<a
className="dib gray2 f9 inter absolute left-0"
href="/"
style={{ top: 14 }}>
<IconHome/>
<span
className="ml2 white-d v-top lh-title"
style={{ paddingTop: 3 }}>
Home
</span>
</a>
<span
className="f9 white-d inter dib"
style={{
verticalAlign: "text-top",
paddingTop: 3
}}>
{title}
</span>
<div className="absolute right-0 lh-copy" style={{ top: 8 }}>
<Sigil
ship={"~" + window.ship}
classes="v-mid mix-blend-diff"
size={16}
color={"#000000"}
/>
<span className="mono white-d f9 ml2 c-default">{cite(window.ship)}</span>
</div>
</div>
);
}
}

View File

@ -0,0 +1,15 @@
import React, { Component } from "react";
export class IconHome extends Component {
render() {
let classes = !!this.props.classes ? this.props.classes : "";
return (
<img
className={"invert-d " + classes}
src="/~%APPNAME%/img/Home.png"
width={16}
height={16}
/>
);
}
}

View File

@ -0,0 +1,9 @@
import React, { Component } from 'react';
export class IconSpinner extends Component {
render() {
return (
<div className="spinner-pending"></div>
);
}
}

View File

@ -0,0 +1,32 @@
import React, { Component } from 'react';
import { sigil, reactRenderer } from 'urbit-sigil-js';
export class Sigil extends Component {
render() {
const { props } = this;
let classes = props.classes || "";
if (props.ship.length > 14) {
return (
<div
className={"bg-black dib " + classes}
style={{ width: props.size, height: props.size }}>
</div>
);
} else {
return (
<div className={"dib " + classes} style={{ flexBasis: 32, backgroundColor: props.color }}>
{sigil({
patp: props.ship,
renderer: reactRenderer,
size: props.size,
colors: [props.color, "white"]
})}
</div>
);
}
}
}

View File

@ -0,0 +1,33 @@
import React, { Component } from 'react';
import { BrowserRouter, Route } from "react-router-dom";
import _ from 'lodash';
import { HeaderBar } from "./lib/header-bar.js"
export class Root extends Component {
constructor(props) {
super(props);
}
render() {
return (
<BrowserRouter>
<div className="absolute h-100 w-100 bg-gray0-d ph4-m ph4-l ph4-xl pb4-m pb4-l pb4-xl">
<HeaderBar/>
<Route exact path="/~%APPNAME%" render={ () => {
return (
<div className="cf w-100 flex flex-column pa4 ba-m ba-l ba-xl b--gray2 br1 h-100 h-100-minus-40-s h-100-minus-40-m h-100-minus-40-l h-100-minus-40-xl f9 white-d overflow-x-hidden">
<h1 className="mt0 f8 fw4">%APPNAME%</h1>
<p className="lh-copy measure pt3">Welcome to your Landscape application.</p>
<p className="lh-copy measure pt3">To get started, edit <code>src/index.js</code>, <code>tile/tile.js</code> or <code>urbit/app/%APPNAME%.hoon</code> and <code>|commit %home</code> on your Urbit ship to see your changes.</p>
<a className="black no-underline db f8 pt3" href="https://urbit.org/docs">-> Read the docs</a>
</div>
)}}
/>
</div>
</BrowserRouter>
)
}
}

82
full/src/js/lib/util.js Normal file
View File

@ -0,0 +1,82 @@
import _ from 'lodash';
import classnames from 'classnames';
export function uuid() {
let str = "0v"
str += Math.ceil(Math.random()*8)+"."
for (var i = 0; i < 5; i++) {
let _str = Math.ceil(Math.random()*10000000).toString(32);
_str = ("00000"+_str).substr(-5,5);
str += _str+".";
}
return str.slice(0,-1);
}
export function isPatTa(str) {
const r = /^[a-z,0-9,\-,\.,_,~]+$/.exec(str)
return !!r;
}
/*
Goes from:
~2018.7.17..23.15.09..5be5 // urbit @da
To:
(javascript Date object)
*/
export function daToDate(st) {
var dub = function(n) {
return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString();
};
var da = st.split('..');
var bigEnd = da[0].split('.');
var lilEnd = da[1].split('.');
var ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(lilEnd[0])}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
return new Date(ds);
}
/*
Goes from:
(javascript Date object)
To:
~2018.7.17..23.15.09..5be5 // urbit @da
*/
export function dateToDa(d, mil) {
  var fil = function(n) {
    return n >= 10 ? n : "0" + n;
  };
  return (
    `~${d.getUTCFullYear()}.` +
    `${(d.getUTCMonth() + 1)}.` +
    `${fil(d.getUTCDate())}..` +
    `${fil(d.getUTCHours())}.` +
    `${fil(d.getUTCMinutes())}.` +
    `${fil(d.getUTCSeconds())}` +
`${mil ? "..0000" : ""}`
  );
}
export function deSig(ship) {
return ship.replace('~', '');
}
// trim patps to match dojo, chat-cli
export function cite(ship) {
let patp = ship, shortened = "";
if (patp.startsWith("~")) {
patp = patp.substr(1);
}
// comet
if (patp.length === 56) {
shortened = "~" + patp.slice(0, 6) + "_" + patp.slice(50, 56);
return shortened;
}
// moon
if (patp.length === 27) {
shortened = "~" + patp.slice(14, 20) + "^" + patp.slice(21, 27);
return shortened;
}
return `~${patp}`;
}

View File

@ -0,0 +1,11 @@
import _ from 'lodash';
export class ConfigReducer {
reduce(json, state) {
let data = _.get(json, '%APPNAME%', false);
if (data) {
state.inbox = data.inbox;
}
}
}

View File

@ -0,0 +1,11 @@
import _ from 'lodash';
export class InitialReducer {
reduce(json, state) {
let data = _.get(json, 'initial', false);
if (data) {
state.inbox = data.inbox;
}
}
}

View File

@ -0,0 +1,17 @@
import _ from 'lodash';
export class UpdateReducer {
reduce(json, state) {
let data = _.get(json, 'update', false);
if (data) {
this.reduceInbox(_.get(data, 'inbox', false), state);
}
}
reduceInbox(inbox, state) {
if (inbox) {
state.inbox = inbox;
}
}
}

35
full/src/js/store.js Normal file
View File

@ -0,0 +1,35 @@
import { InitialReducer } from '/reducers/initial';
import { ConfigReducer } from '/reducers/config';
import { UpdateReducer } from '/reducers/update';
class Store {
constructor() {
this.state = {
inbox: {}
};
this.initialReducer = new InitialReducer();
this.configReducer = new ConfigReducer();
this.updateReducer = new UpdateReducer();
this.setState = () => { };
}
setStateHandler(setState) {
this.setState = setState;
}
handleEvent(data) {
let json = data.data;
console.log(json);
this.initialReducer.reduce(json, this.state);
this.configReducer.reduce(json, this.state);
this.updateReducer.reduce(json, this.state);
this.setState(this.state);
}
}
export let store = new Store();
window.store = store;

View File

@ -0,0 +1,34 @@
import { api } from '/api';
import { store } from '/store';
import urbitOb from 'urbit-ob';
export class Subscription {
start() {
if (api.authTokens) {
// this.initialize%APPNAME%();
} else {
console.error("~~~ ERROR: Must set api.authTokens before operation ~~~");
}
}
// initialize%APPNAME%() {
// api.bind('/primary', 'PUT', api.authTokens.ship, '%APPNAME%',
// this.handleEvent.bind(this),
// this.handleError.bind(this));
// }
handleEvent(diff) {
store.handleEvent(diff);
}
handleError(err) {
console.error(err);
api.bind('/primary', 'PUT', api.authTokens.ship, '%APPNAME%',
this.handleEvent.bind(this),
this.handleError.bind(this));
}
}
export let subscription = new Subscription();

1
full/src/js/vendor/sigils-1.2.5.js vendored Normal file

File diff suppressed because one or more lines are too long

20
full/tile/tile.js Normal file
View File

@ -0,0 +1,20 @@
import React, { Component } from 'react';
import _ from 'lodash';
export default class %APPNAME%Tile extends Component {
render() {
return (
<div className="w-100 h-100 relative bg-white bg-gray0-d ba b--black b--gray1-d">
<a className="w-100 h-100 db pa2 no-underline" href="/~%APPNAME%">
<p className="black white-d absolute f9" style={{ left: 8, top: 8 }}>%APPNAME%</p>
<img className="absolute" src="/~%APPNAME%/img/Tile.png" style={{top: 39, left: 39}}/>
</a>
</div>
);
}
}
window.%APPNAME%Tile = %APPNAME%Tile;

113
full/urbit/app/smol.hoon Normal file
View File

@ -0,0 +1,113 @@
/+ *server, default-agent
/= index
/^ octs
/; as-octs:mimes:html
/: /===/app/%APPNAME%/index
/| /html/
/~ ~
==
/= tile-js
/^ octs
/; as-octs:mimes:html
/: /===/app/%APPNAME%/js/tile
/| /js/
/~ ~
==
/= script
/^ octs
/; as-octs:mimes:html
/: /===/app/%APPNAME%/js/index
/| /js/
/~ ~
==
/= style
/^ octs
/; as-octs:mimes:html
/: /===/app/%APPNAME%/css/index
/| /css/
/~ ~
==
/= %APPNAME%-png
/^ (map knot @)
/: /===/app/%APPNAME%/img /_ /png/
::
|%
+$ card card:agent:gall
--
^- agent:gall
=<
|_ bol=bowl:gall
+* this .
%APPNAME%-core +>
cc ~(. %APPNAME%-core bol)
def ~(. (default-agent this %|) bol)
::
++ on-init
^- (quip card _this)
=/ launcha [%launch-action !>([%add %%APPNAME% / '/~%APPNAME%/js/tile.js'])]
:_ this
:~ [%pass / %arvo %e %connect [~ /'~%APPNAME%'] %%APPNAME%]
[%pass /%APPNAME% %agent [our.bol %launch] %poke launcha]
==
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?> (team:title our.bol src.bol)
?+ mark (on-poke:def mark vase)
%handle-http-request
=+ !<([eyre-id=@ta =inbound-request:eyre] vase)
:_ this
%+ give-simple-payload:app eyre-id
%+ require-authorization:app inbound-request
poke-handle-http-request:cc
::
==
::
++ on-watch
|= =path
^- (quip card:agent:gall _this)
?: ?=([%http-response *] path)
`this
?. =(/ path)
(on-watch:def path)
[[%give %fact ~ %json !>(*json)]~ this]
::
++ on-agent on-agent:def
::
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?. ?=(%bound +<.sign-arvo)
(on-arvo:def wire sign-arvo)
[~ this]
::
++ on-save on-save:def
++ on-load on-load:def
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-fail on-fail:def
--
::
::
|_ bol=bowl:gall
::
++ poke-handle-http-request
|= =inbound-request:eyre
^- simple-payload:http
=+ url=(parse-request-line url.request.inbound-request)
?+ site.url not-found:gen
[%'~%APPNAME%' %css %index ~] (css-response:gen style)
[%'~%APPNAME%' %js %tile ~] (js-response:gen tile-js)
[%'~%APPNAME%' %js %index ~] (js-response:gen script)
::
[%'~%APPNAME%' %img @t *]
=/ name=@t i.t.t.site.url
=/ img (~(get by %APPNAME%-png) name)
?~ img
not-found:gen
(png-response:gen (as-octs:mimes:html u.img))
::
[%'~%APPNAME%' *] (html-response:gen index)
==
::
--

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,16 @@
<!doctype html>
<html>
<head>
<title>%APPNAME%</title>
<meta charset="utf-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
<link rel="stylesheet" href="/~%APPNAME%/css/index.css" />
</head>
<body>
<div id="root" />
<script src="/~/channel/channel.js"></script>
<script src="/~modulo/session.js"></script>
<script src="/~%APPNAME%/js/index.js"></script>
</body>
</html>

82
gulpfile.js Normal file
View File

@ -0,0 +1,82 @@
var gulp = require('gulp');
var rollup = require('gulp-better-rollup');
var sucrase = require('@sucrase/gulp-plugin');
var minify = require('gulp-minify');
var resolve = require('rollup-plugin-node-resolve');
var commonjs = require('rollup-plugin-commonjs');
var rootImport = require('rollup-plugin-root-import');
var globals = require('rollup-plugin-node-globals');
/***
Main config options
***/
var urbitrc = require('./.urbitrc');
/***
End main config options
***/
gulp.task('tile-jsx-transform', function(cb) {
return gulp.src('tile/**/*.js')
.pipe(sucrase({
transforms: ['jsx']
}))
.pipe(gulp.dest('dist'));
});
gulp.task('tile-js-imports', function(cb) {
return gulp.src('dist/tile.js')
.pipe(rollup({
plugins: [
commonjs({
namedExports: {
'node_modules/react/index.js': [ 'Component' ],
}
}),
rootImport({
root: `${__dirname}/dist/js`,
useEntry: 'prepend',
extensions: '.js'
}),
globals(),
resolve()
]
}, 'umd'))
.on('error', function(e){
console.log(e);
cb();
})
.pipe(gulp.dest('./urbit/app/%APPNAME%/js/'))
.on('end', cb);
});
gulp.task('tile-js-minify', function () {
return gulp.src('./urbit/app/%APPNAME%/js/tile.js')
.pipe(minify())
.pipe(gulp.dest('./urbit/app/%APPNAME%/js/'));
});
gulp.task('urbit-copy', function () {
let ret = gulp.src('urbit/**/*');
urbitrc.URBIT_PIERS.forEach(function(pier) {
ret = ret.pipe(gulp.dest(pier));
});
return ret;
});
gulp.task('tile-js-bundle-dev', gulp.series('tile-jsx-transform', 'tile-js-imports'));
gulp.task('tile-js-bundle-prod',
gulp.series('tile-jsx-transform', 'tile-js-imports', 'tile-js-minify'));
gulp.task('bundle-prod', gulp.series('tile-js-bundle-prod', 'urbit-copy'));
gulp.task('default', gulp.series('tile-js-bundle-dev', 'urbit-copy'));
gulp.task('watch', gulp.series('default', function() {
gulp.watch('tile/**/*.js', gulp.parallel('tile-js-bundle-dev'));
gulp.watch('urbit/**/*', gulp.parallel('urbit-copy'));
}));

191
install.js Normal file
View File

@ -0,0 +1,191 @@
const prompt = require('prompt')
const replace = require('replace-in-file')
const fs = require('fs-extra');
var Promise = require('promise');
var path = require('path');
// Making the text input a bit legible.
prompt.colors = false
prompt.message = ""
// The text input takes a "result" object and passes it to one of two functions to do the logistics.
prompt.get([{
name: 'appName',
required: true,
description: "What's the name of your application? Lowercase and no spaces, please.",
message: "Lowercase and no spaces, please.",
conform: function(value) {
return /^[a-z0-9]+((\-[a-z0-9]+){1,})?$/g.test(value)
}
},
{
name: 'type',
required: true,
description: "Is your app just a tile, or a full application? (tile/full)",
message: "Please specify 'tile' or 'full'.",
conform: function(value) {
if ((value == "tile") || (value == "full")) return true
return false
}
},
{
name: 'pier',
required: true,
description: "Where is your Urbit pier's desk located? For example, /Users/dev/zod/home"
}], function (err, result) {
if (result.type == "tile") setupTile(result)
else if (result.type == "full") setupFull(result)
}
)
// Delete the 'full' app folder and rename the tile-only files.
const setupTile = function (result) {
deleteFolderRecursive('full')
let deHyphenatedName = result.appName.indexOf('-') > -1 ? result.appName.replace(/-/g, "") : result.appName
fs.renameSync('urbit/app/smol.hoon', 'urbit/app/' + deHyphenatedName + '.hoon')
// Make a copy of the name without hyphens for the JS naming.
let capitalisedAppName = deHyphenatedName.charAt(0).toUpperCase() + deHyphenatedName.slice(1)
let appNameOptions = {
files: ['gulpfile.js', 'urbit/app/' + deHyphenatedName + '.hoon'],
from: /%APPNAME%/g,
to: deHyphenatedName
}
let appNamewithCapitals = {
files: 'tile/tile.js',
from: [/%APPNAME%Tile/g, /%APPNAME%/g],
to: [deHyphenatedName + "Tile", capitalisedAppName]
}
let urbitPierOptions = {
files: '.urbitrc',
from: "%URBITPIER%",
to: result.pier
}
replace(appNameOptions).then(changedFiles => console.log(changedFiles)).catch(err => console.error(err))
replace(appNamewithCapitals).then(changedFiles => console.log(changedFiles)).catch(err => console.error(err))
replace(urbitPierOptions).then(changedFiles => console.log(changedFiles)).catch(err => console.error(err))
console.log("All done! Happy hacking.")
}
// Delete the tile-specific files and move the full application to root. Rename everything as necessary.
const deleteFolderRecursive = function (path) {
if (fs.existsSync(path)) {
fs.readdirSync(path).forEach(function (file, index) {
var curPath = path + "/" + file;
if (fs.lstatSync(curPath).isDirectory()) {
deleteFolderRecursive(curPath);
} else {
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(path);
}
};
var promiseAllWait = function (promises) {
// this is the same as Promise.all(), except that it will wait for all promises to fulfill before rejecting
var all_promises = [];
for (var i_promise = 0; i_promise < promises.length; i_promise++) {
all_promises.push(
promises[i_promise]
.then(function (res) {
return { res: res };
}).catch(function (err) {
return { err: err };
})
);
}
return Promise.all(all_promises)
.then(function (results) {
return new Promise(function (resolve, reject) {
var is_failure = false;
var i_result;
for (i_result = 0; i_result < results.length; i_result++) {
if (results[i_result].err) {
is_failure = true;
break;
} else {
results[i_result] = results[i_result].res;
}
}
if (is_failure) {
reject(results[i_result].err);
} else {
resolve(results);
}
});
});
};
var movePromiser = function (from, to, records) {
return fs.move(from, to)
.then(function () {
records.push({ from: from, to: to });
});
};
var moveDir = function (from_dir, to_dir, callback) {
return fs.readdir(from_dir)
.then(function (children) {
return fs.ensureDir(to_dir)
.then(function () {
var move_promises = [];
var moved_records = [];
var child;
for (var i_child = 0; i_child < children.length; i_child++) {
child = children[i_child];
move_promises.push(movePromiser(
path.join(from_dir, child),
path.join(to_dir, child),
moved_records
));
}
return promiseAllWait(move_promises)
.catch(function (err) {
var undo_move_promises = [];
for (var i_moved_record = 0; i_moved_record < moved_records.length; i_moved_record++) {
undo_move_promises.push(fs.move(moved_records[i_moved_record].to, moved_records[i_moved_record].from));
}
return promiseAllWait(undo_move_promises)
.then(function () {
throw err;
});
});
}).then(function () {
return fs.rmdir(from_dir);
});
}).then(callback);
};
const setupFull = function (result) {
deleteFolderRecursive('tile')
deleteFolderRecursive('urbit')
fs.unlinkSync('gulpfile.js')
let deHyphenatedName = result.appName.indexOf('-') > -1 ? result.appName.replace(/-/g, "") : result.appName
moveDir('full', './', function() {
fs.renameSync('urbit/app/smol.hoon', 'urbit/app/' + deHyphenatedName + '.hoon')
fs.renameSync('urbit/app/smol/', 'urbit/app/' + deHyphenatedName)
let urbitPierOptions = {
files: '.urbitrc',
from: "%URBITPIER%",
to: result.pier
}
replace(urbitPierOptions).then(changedFiles => console.log(changedFiles)).catch(err => console.error(err))
let appNameOptions = {
files: ['gulpfile.js', 'urbit/app/' + deHyphenatedName + '.hoon', 'tile/tile.js',
'src/js/api.js', 'src/js/subscription.js', 'src/js/components/root.js',
'src/js/reducers/config.js', 'urbit/app/' + deHyphenatedName + '/index.html', 'src/js/components/lib/icons/icon-home.js'
],
from: /%APPNAME%/g,
to: deHyphenatedName
}
replace(appNameOptions).then(changedFiles => console.log(changedFiles)).catch(err => console.error(err))
})
}

6100
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "create-landscape-app",
"version": "3.0.0",
"description": "Get started with a Landscape application.",
"main": "node install.js",
"scripts": {
"start": "node install.js",
"build": "gulp",
"serve": "gulp watch"
},
"author": "Tlon Corp",
"license": "MIT",
"repository": "https://github.com/urbit/create-landscape-app",
"devDependencies": {
"@sucrase/gulp-plugin": "^2.0.0",
"cssnano": "^4.1.10",
"gulp": "^4.0.0",
"gulp-better-rollup": "^4.0.1",
"gulp-cssimport": "^7.0.0",
"gulp-minify": "^3.1.0",
"gulp-postcss": "^8.0.0",
"rollup": "^1.6.0",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-root-import": "^0.2.3",
"sucrase": "^3.8.0"
},
"dependencies": {
"classnames": "^2.2.6",
"fs-extra": "^8.1.0",
"lodash": "^4.17.11",
"moment": "^2.20.1",
"mousetrap": "^1.6.3",
"mv": "^2.1.1",
"promise": "^8.0.3",
"prompt": "^1.0.0",
"react": "^16.5.2",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.0",
"replace-in-file": "^4.1.1",
"urbit-ob": "^4.1.2",
"urbit-sigil-js": "^1.3.13"
},
"resolutions": {
"natives": "1.1.3"
}
}

17
tile/tile.js Normal file
View File

@ -0,0 +1,17 @@
import React, { Component } from 'react';
import _ from 'lodash';
export default class %APPNAME%Tile extends Component {
render() {
return (
<div className="w-100 h-100 relative bg-white bg-gray0-d ba b--black b--gray1-d">
<p className="black white-d absolute f9" style={{ left: 8, top: 8 }}>%APPNAME%</p>
</div>
);
}
}
window.%APPNAME%Tile = %APPNAME%Tile;

71
urbit/app/smol.hoon Normal file
View File

@ -0,0 +1,71 @@
/+ *server, default-agent, verb
/= tile-js
/^ octs
/; as-octs:mimes:html
/: /===/app/%APPNAME%/js/tile
/| /js/
/~ ~
==
=, format
::
%+ verb |
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
::
++ on-init
^- (quip card:agent:gall _this)
=/ launcha
[%launch-action !>([%add %%APPNAME% /%APPNAME%tile '/~%APPNAME%/js/tile.js'])]
:_ this
:~ [%pass / %arvo %e %connect [~ /'~%APPNAME%'] %%APPNAME%]
[%pass /%APPNAME% %agent [our.bowl %launch] %poke launcha]
==
++ on-save on-save:def
++ on-load on-load:def
++ on-poke
|= [=mark =vase]
^- (quip card:agent:gall _this)
?. ?=(%handle-http-request mark)
(on-poke:def mark vase)
=+ !<([eyre-id=@ta =inbound-request:eyre] vase)
:_ this
%+ give-simple-payload:app eyre-id
%+ require-authorization:app inbound-request
|= =inbound-request:eyre
=/ request-line (parse-request-line url.request.inbound-request)
=/ back-path (flop site.request-line)
=/ name=@t
=/ back-path (flop site.request-line)
?~ back-path
''
i.back-path
::
?~ back-path
not-found:gen
?: =(name 'tile')
(js-response:gen tile-js)
not-found:gen
::
++ on-watch
|= =path
^- (quip card:agent:gall _this)
?: ?=([%http-response *] path)
`this
?. =([/%APPNAME%tile *] path)
(on-watch:def path)
[[%give %fact ~ %json !>(*json)]~ this]
::
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-agent on-agent:def
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card:agent:gall _this)
?. ?=(%bound +<.sign-arvo)
(on-arvo:def wire sign-arvo)
[~ this]
::
++ on-fail on-fail:def
--