Compare commits

..

7 commits

23 changed files with 3496 additions and 40 deletions

View file

@ -2,5 +2,8 @@
"name": "SilverStripe Environment",
"dockerComposeFile": "docker-compose.yml",
"service": "silverstripe",
"workspaceFolder": "/vscode/src/mantra"
"workspaceFolder": "/vscode/src/mantra",
"extensions": [
"ms-vscode.vscode-typescript-tslint-plugin"
]
}

View file

@ -3,7 +3,7 @@ version: '3'
services:
silverstripe:
image: manuth/silverstripe-dev
env_file: .env
env_file: devcontainer.env
ports:
- 8080:80
volumes:
@ -11,6 +11,6 @@ services:
- ../test/website:/var/www/html
db:
image: mysql:5
env_file: .env
env_file: devcontainer.env
volumes:
- ./mysql-data:/var/lib/mysql

15
.gitignore vendored
View file

@ -51,9 +51,15 @@ typings/
# Output of 'npm pack'
*.tgz
# Output of 'composer archive'
*.tar
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
@ -77,3 +83,12 @@ vendor/
# MySQL cache
.devcontainer/mysql-data
# Build files
/javascript/
/css/
/templates/
/assets/
# Test-theme
/test/website/themes/mantra

240
.gulp/Settings.ts Normal file
View file

@ -0,0 +1,240 @@
import Path = require("upath");
/**
* Provides settings for building the project.
*/
export class Settings
{
/**
* A value indicating whether the project should be built in watched mode.
*/
public Watch = false;
/**
* The target of the project-build.
*/
public Target: string;
/**
* The path to the source-code root.
*/
private sourceRoot = "src";
/**
* The path to the root of the typescript-project.
*/
private typeScriptProjectRoot = "App";
/**
* The path to the root of the typescript-source.
*/
private typeScriptSourceRoot = "src";
/**
* The path to save the javascript-code to.
*/
private libraryPath = "javascript";
/**
* The path to the root of the theme-source.
*/
private themeSource = "Theme";
/**
* The path to save the css-code to.
*/
private stylePath = "css";
/**
* The path to the root of the template-source.
*/
private templateSource = "Templates";
/**
* The path to save the templates to.
*/
private templatePath = "templates";
/**
* The path to the test-directory.
*/
private testPath = "test";
/**
* The path to the test-website.
*/
private testWebsitePath = "website";
/**
* Initializes a new instance of the `Settings` class.
*
* @param target
* The target of the project-build.
*/
public constructor(target: string)
{
this.Target = target;
}
/**
* A value indicating whether the debug-mode is enabled.
*/
public get Debug()
{
return this.Target === "Debug";
}
/**
* Creates a path relative to the root of the solution.
*
* @param path
* The path to join.
*
* @return
* The joined path.
*/
public RootPath(...path: string[])
{
return Path.join(Path.dirname(__dirname), ...path);
}
/**
* Creates a path relative to the root of the source-code.
*
* @param path
* The path to join.
*
* @returns
* The joined path.
*/
public SourceRoot(...path: string[])
{
return this.RootPath(this.sourceRoot, ...path);
}
/**
* Creates a path relative to the root of the typescript-project.
*
* @param path
* The path to join.
*
* @returns
* The joined path.
*/
public TypeScriptProjectRoot(...path: string[])
{
return this.SourceRoot(this.typeScriptProjectRoot, ...path);
}
/**
* Creates a path relative to the root of the typescript-source.
*
* @param path
* The path to join.
*
* @returns
* The joined path.
*/
public TypeScriptSourceRoot(...path: string[])
{
return this.TypeScriptProjectRoot(this.typeScriptSourceRoot, ...path);
}
/**
* Creates a path relative to the directory to save the javascript-code to.
*
* @param path
* The path to join.
*
* @returns
* The joined path.
*/
public LibraryPath(...path: string[])
{
return this.RootPath(this.libraryPath, ...path);
}
/**
* Creates a path relative to the root of the theme-source.
*
* @param path
* The path to join.
*
* @returns
* The joined path.
*/
public ThemeSource(...path: string[])
{
return this.SourceRoot(this.themeSource, ...path);
}
/**
* Creates a path relative to the directory to save the css-code to.
*
* @param path
* The path to join.
*
* @returns
* The joined path.
*/
public StylePath(...path: string[])
{
return this.RootPath(this.stylePath, ...path);
}
/**
* Creates a path relative to the root of the template-source.
*
* @param path
* The path to join.
*
* @returns
* The joined path.
*/
public TemplateSource(...path: string[])
{
return this.SourceRoot(this.templateSource, ...path);
}
/**
* Creates a path relative to the directory to save the templates to.
*
* @param path
* The path to join.
*
* @returns
* The joined path.
*/
public TemplatePath(...path: string[])
{
return this.RootPath(this.templatePath, ...path);
}
/**
* Creates a path relative to the test-directory.
*
* @param path
* The path to join.
*
* @returns
* The joined path.
*/
public TestPath(...path: string[])
{
return this.RootPath(this.testPath, ...path);
}
/**
* Creates a path relative to the test-website.
*
* @param path
* The path to join.
*
* @returns
* The joined path.
*/
public TestWebsitePath(...path: string[])
{
return this.TestPath(this.testWebsitePath, ...path);
}
}

View file

@ -5,7 +5,8 @@ import { TaskFunction } from "gulp";
*/
declare global
{
interface Function extends TaskFunction
// tslint:disable-next-line: completed-docs
interface Function extends Partial<TaskFunction>
{ }
}

15
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"adrianhumphreys.silverstripe",
"ms-vscode.vscode-typescript-tslint-plugin",
"ms-vscode-remote.remote-containers"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": [
]
}

8
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"javascript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": false,
"javascript.format.placeOpenBraceOnNewLineForControlBlocks": true,
"javascript.format.placeOpenBraceOnNewLineForFunctions": true,
"typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": false,
"typescript.format.placeOpenBraceOnNewLineForControlBlocks": true,
"typescript.format.placeOpenBraceOnNewLineForFunctions": true
}

View file

@ -36,18 +36,26 @@
"archive": {
"exclude": [
"/*.tar",
".devcontainer",
".gulp",
".gitignore",
".vscode/",
"gulpfile.ts",
"node_modules/",
"package-lock.json",
"package.json",
"test/"
"src/",
"test/",
"tsconfig.json",
"tslint.json"
]
},
"extra": {
"scripts-dev": {
"post-install-cmd": [
"npm install"
"npm install",
"@initialize",
"composer --working-dir=./test/website install"
]
}
}

2
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "22068a23fc6b360469aeccc3a344ead6",
"content-hash": "848468c4f4ad00ed3629fa3cbf2bea5b",
"packages": [],
"packages-dev": [
{

View file

@ -1,11 +1,363 @@
import browserify = require("browserify");
import log = require("fancy-log");
import FileSystem = require("fs-extra");
import { TaskFunction } from "gulp";
import gulp = require("gulp");
import gulpIf = require("gulp-if");
import rename = require("gulp-rename");
import sass = require("gulp-sass");
import terser = require("gulp-terser");
import merge = require("merge-stream");
import minimist = require("minimist");
import { Server, Socket } from "net";
import PromiseQueue = require("promise-queue");
import { parseArgsStringToArgv } from "string-argv";
import Path = require("upath");
import buffer = require("vinyl-buffer");
import vinylSourceStream = require("vinyl-source-stream");
import Watchify = require("watchify");
import { Settings } from "./.gulp/Settings";
import "./.gulp/TaskFunction";
/**
* The port to listen for stop-requests.
*/
const watchConnectorPort = 50958;
/**
* The message that is printed when starting the compilation in watch mode.
*/
const watchStartMessage = "Starting compilation in watch mode...";
/**
* The message that is printed when starting an incremental compilation.
*/
const incrementalMessage = "File change detected. Starting incremental compilation...";
/**
* Generates the message that is printed after finishing a compilation in watch mode.
*
* @param errorCount
* The number of errors which occurred.
*/
const watchFinishMessage = (errorCount: number) =>
{
return `Found ${errorCount} errors. Watching for file changes.`;
};
/**
* The arguments passed by the user.
*/
let options = ParseArgs(process.argv.slice(2));
/**
* Parses the specified arguments.
*
* @param args
* The arguments to parse.
*/
function ParseArgs(args: string[])
{
return minimist(
args,
{
string: [
"target"
],
alias: {
target: "t"
},
default: {
target: "Debug"
}
});
}
/**
* The settings for building the project.
*/
let settings = new Settings(options["target"]);
/**
* Initializes the project.
*/
export function Initialize(done: () => void)
export async function Initialize()
{
done();
let directories = [
"javascript",
"css",
"templates",
"assets"
];
let themePath = settings.TestWebsitePath("themes", Path.basename(settings.RootPath()));
for (let directory of directories)
{
await FileSystem.emptyDir(settings.RootPath(directory));
}
if (await FileSystem.pathExists(themePath))
{
await FileSystem.remove(themePath);
}
await require("create-symlink")(settings.RootPath(), themePath, { type: "junction" });
}
Initialize.description = "Initializes the project.";
Initialize.description = "Initializes the project.";
/**
* Builds the project in watched mode.
*/
export let Watch: TaskFunction = (done) =>
{
settings.Watch = true;
Build();
let server = new Server(
(socket) =>
{
socket.on(
"data",
(data) =>
{
let args = parseArgsStringToArgv(data.toString());
socket.destroy();
if (args[0] === "stop")
{
let options = ParseArgs(args.slice(1));
if (options["target"] === settings.Target)
{
server.close();
done();
process.exit();
}
}
});
});
server.listen(watchConnectorPort);
};
Watch.description = "Builds the project in watched mode.";
/**
* Builds the project.
*/
export async function Build()
{
if (settings.Watch)
{
log.info(watchStartMessage);
gulp.watch(settings.ThemeSource("**"), Theme);
gulp.watch(settings.TemplateSource("**"), Templates);
}
await Promise.all(
[
Library(),
Theme(),
Templates()
]);
}
/**
* Builds the TypeScript- and JavaScript-library.
*/
export async function Library()
{
let streams: Array<Promise<NodeJS.ReadWriteStream>> = [];
let queue = new PromiseQueue();
let tsConfigFile = settings.TypeScriptProjectRoot("tsconfig.json");
let tsConfig = require(tsConfigFile);
let optionBase: browserify.Options = {
...Watchify.args,
node: true,
ignoreMissing: true,
debug: settings.Debug
};
{
let errorMessages: string[] = [];
let files = (tsConfig.files as string[]).map(
(file) => Path.relative(settings.TypeScriptSourceRoot(), settings.TypeScriptProjectRoot(file)));
for (let file of files)
{
let bundler = browserify(
{
...optionBase,
basedir: settings.LibraryPath(Path.dirname(file)),
entries: [
settings.TypeScriptSourceRoot(file)
],
standalone: Path.join(Path.dirname(file), Path.parse(file).name)
});
if (settings.Watch)
{
bundler = Watchify(bundler);
}
bundler.plugin(
require("tsify"),
{
project: tsConfigFile
});
let bundle = async () =>
{
return new Promise<NodeJS.ReadWriteStream>(
(resolve) =>
{
let stream = bundler.bundle().on(
"error",
(error) =>
{
let message: string = error.message;
if (!errorMessages.includes(message))
{
errorMessages.push(message);
log.error(message);
}
}
).pipe(
vinylSourceStream(Path.changeExt(file, "js"))
).pipe(
buffer()
).pipe(
gulpIf(
!settings.Debug,
terser()
)
).pipe(
gulp.dest(settings.LibraryPath())
);
stream.on(
"end",
() =>
{
if (settings.Watch && ((queue.getQueueLength() + queue.getPendingLength()) === 1))
{
log.info(watchFinishMessage(errorMessages.length));
}
errorMessages.splice(0, errorMessages.length);
resolve(stream);
});
});
};
if (settings.Watch)
{
bundler.on(
"update",
() =>
{
if ((queue.getQueueLength() + queue.getPendingLength()) === 0)
{
log.info(incrementalMessage);
}
queue.add(
async () =>
{
return bundle();
});
});
}
let build = () => queue.add(bundle);
build.displayName = Build.displayName;
build.description = Build.description;
streams.push(build());
}
}
return merge(await Promise.all(streams)) as NodeJS.ReadWriteStream;
}
Library.description = "Builds the TypeScript- and JavaScript-library.";
/**
* Builds the theme.
*/
export async function Theme()
{
return gulp.src(
settings.ThemeSource("main.scss"),
{
sourcemaps: settings.Debug,
base: settings.StylePath()
}).pipe(
sass({ importer: require("node-sass-tilde-importer") })
).pipe(
rename(
(parsedPath) =>
{
parsedPath.dirname = "";
parsedPath.basename = "mantra";
})
).pipe(
gulp.dest(
settings.StylePath(),
settings.Debug ?
{
sourcemaps: true
}
:
undefined)
);
}
Theme.description = "Builds the theme.";
/**
* Builds the templates.
*/
export function Templates()
{
return gulp.src(
settings.TemplateSource("**")).pipe(
gulp.dest(settings.TemplatePath()));
}
Templates.description = "Builds the templates.";
/**
* Stops a watch-task.
*/
export async function Stop()
{
try
{
await new Promise(
(resolve, reject) =>
{
let client = new Socket();
client.connect(
watchConnectorPort,
"localhost",
async () =>
{
client.write(`stop -t ${settings.Target}`);
});
client.on("close", resolve);
client.on("error", reject);
});
}
catch
{
log.info("The specified task is not running.");
}
}
Stop.description = "Stops a watch-task";

2694
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,9 +5,50 @@
},
"dependencies": {},
"devDependencies": {
"@manuth/tsconfig": "^1.2.2",
"@manuth/tslint-presets": "^1.0.4",
"@types/bootstrap": "^4.3.1",
"@types/browserify": "^12.0.36",
"@types/fancy-log": "^1.3.1",
"@types/fs-extra": "^8.0.0",
"@types/gulp": "^4.0.6",
"@types/gulp-if": "0.0.33",
"@types/gulp-rename": "0.0.33",
"@types/gulp-sass": "^4.0.0",
"@types/gulp-terser": "^1.2.0",
"@types/jquery": "^3.3.31",
"@types/merge-stream": "^1.1.2",
"@types/minimist": "^1.2.0",
"@types/node": "^12.7.11",
"@types/promise-queue": "^2.2.0",
"@types/vinyl-buffer": "^1.0.0",
"@types/vinyl-source-stream": "0.0.30",
"@types/watchify": "^3.7.4",
"bootstrap": "^4.3.1",
"browserify": "^16.5.0",
"create-symlink": "^1.0.0",
"fancy-log": "^1.3.3",
"fs-extra": "^8.1.0",
"gulp": "^4.0.2",
"gulp-if": "^3.0.0",
"gulp-rename": "^1.4.0",
"gulp-sass": "^4.0.2",
"gulp-terser": "^1.2.0",
"jquery": "^3.4.1",
"merge-stream": "^2.0.0",
"minimist": "^1.2.0",
"node-sass-tilde-importer": "^1.0.2",
"popper.js": "^1.15.0",
"promise-queue": "^2.2.5",
"string-argv": "^0.3.1",
"ts-node": "^8.4.1",
"typescript": "^3.6.3"
"tsify": "^4.0.1",
"tslint": "^5.20.0",
"typescript": "^3.6.3",
"typescript-tslint-plugin": "^0.5.4",
"upath": "^1.2.0",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"watchify": "^3.11.1"
}
}

3
src/App/src/main.ts Normal file
View file

@ -0,0 +1,3 @@
import "bootstrap";
import "jquery";
import "popper.js";

13
src/App/tsconfig.json Normal file
View file

@ -0,0 +1,13 @@
{
"files": [
"src/main.ts"
],
"compilerOptions": {
"rootDir": "src",
"lib": [
"es7",
"dom"
],
"target": "es5"
}
}

View file

45
src/Templates/Page.ss Normal file
View file

@ -0,0 +1,45 @@
<!doctype html>
<html class="h-100">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<% base_tag %>
$MetaTags()
<% require themedCSS("mantra") %>
<% require themedJavascript("mantra") %>
</head>
<body class="d-flex flex-column h-100">
<% if $Menu(1) %>
<div class="col-1">
<nav>
<div class="active">
<a>
$Title
</a>
<ul class="nav bd-sidenav">
<% loop $Menu(1) %>
<li class="$LinkingMode">
<a href="$Link">
$MenuTitle
</a>
</li>
<% end_loop %>
</ul>
</div>
</nav>
</div>
<% end_if %>
<main role="main" class="col-11 flex-shrink-0">
<div class="container">
<% loop $Menu(1) %>
<li><a class="$LinkingMode" href="$Link" title="Go to the $Title page">$MenuTitle</a></li>
<% end_loop %>
<h1 class="display-4">
$MenuTitle
</h1>
$Content
</div>
</main>
</body>
</html>

1
src/Theme/main.scss Normal file
View file

@ -0,0 +1 @@
@import "~bootstrap/scss/bootstrap";

View file

@ -3,6 +3,6 @@ Name: mytheme
---
SilverStripe\View\SSViewer:
themes:
- 'mantra'
- '$public'
- 'simple'
- '$default'

View file

@ -12,6 +12,11 @@
"phpunit/phpunit": "^5.7"
},
"extra": {
"expose": [
"themes/mantra/assets",
"themes/mantra/css",
"themes/mantra/javascript"
],
"resources-dir": "_resources",
"project-files-installed": [
"app/.htaccess",

View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7ff31cbd9aa5ee75c9624d2bb8c3bc5f",
"content-hash": "0ab3e4aaf71526993c3dbb3ea6179605",
"packages": [
{
"name": "bramus/ansi-php",
@ -2216,16 +2216,16 @@
},
{
"name": "symfony/cache",
"version": "v3.4.31",
"version": "v3.4.32",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache.git",
"reference": "6a9cc3ed7f61c6c2bc042d0594cdb807b6fea62c"
"reference": "5c7bd827617fcb9b13e04e423c31c0fb0bcf0160"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/cache/zipball/6a9cc3ed7f61c6c2bc042d0594cdb807b6fea62c",
"reference": "6a9cc3ed7f61c6c2bc042d0594cdb807b6fea62c",
"url": "https://api.github.com/repos/symfony/cache/zipball/5c7bd827617fcb9b13e04e423c31c0fb0bcf0160",
"reference": "5c7bd827617fcb9b13e04e423c31c0fb0bcf0160",
"shasum": ""
},
"require": {
@ -2282,20 +2282,20 @@
"caching",
"psr6"
],
"time": "2019-08-26T07:52:58+00:00"
"time": "2019-09-29T21:19:44+00:00"
},
{
"name": "symfony/config",
"version": "v3.4.31",
"version": "v3.4.32",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "24a60c0d7ad98a0fa5d1f892e9286095a389404f"
"reference": "717ad66b5257e9752ae3c5722b5810bb4c40b236"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/24a60c0d7ad98a0fa5d1f892e9286095a389404f",
"reference": "24a60c0d7ad98a0fa5d1f892e9286095a389404f",
"url": "https://api.github.com/repos/symfony/config/zipball/717ad66b5257e9752ae3c5722b5810bb4c40b236",
"reference": "717ad66b5257e9752ae3c5722b5810bb4c40b236",
"shasum": ""
},
"require": {
@ -2346,11 +2346,11 @@
],
"description": "Symfony Config Component",
"homepage": "https://symfony.com",
"time": "2019-08-26T07:52:57+00:00"
"time": "2019-09-19T15:32:51+00:00"
},
{
"name": "symfony/filesystem",
"version": "v4.3.4",
"version": "v4.3.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
@ -2400,16 +2400,16 @@
},
{
"name": "symfony/finder",
"version": "v3.4.31",
"version": "v3.4.32",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "1fcad80b440abcd1451767349906b6f9d3961d37"
"reference": "2b6a666d6ff7fb65d10b97d817c8e7930944afb9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/1fcad80b440abcd1451767349906b6f9d3961d37",
"reference": "1fcad80b440abcd1451767349906b6f9d3961d37",
"url": "https://api.github.com/repos/symfony/finder/zipball/2b6a666d6ff7fb65d10b97d817c8e7930944afb9",
"reference": "2b6a666d6ff7fb65d10b97d817c8e7930944afb9",
"shasum": ""
},
"require": {
@ -2445,7 +2445,7 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"time": "2019-08-14T09:39:58+00:00"
"time": "2019-09-01T21:32:23+00:00"
},
{
"name": "symfony/polyfill-apcu",
@ -2686,16 +2686,16 @@
},
{
"name": "symfony/yaml",
"version": "v3.4.31",
"version": "v3.4.32",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "3dc414b7db30695bae671a1d86013d03f4ae9834"
"reference": "768f817446da74a776a31eea335540f9dcb53942"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/3dc414b7db30695bae671a1d86013d03f4ae9834",
"reference": "3dc414b7db30695bae671a1d86013d03f4ae9834",
"url": "https://api.github.com/repos/symfony/yaml/zipball/768f817446da74a776a31eea335540f9dcb53942",
"reference": "768f817446da74a776a31eea335540f9dcb53942",
"shasum": ""
},
"require": {
@ -2741,7 +2741,7 @@
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2019-08-20T13:31:17+00:00"
"time": "2019-09-10T10:38:46+00:00"
},
{
"name": "webonyx/graphql-php",

18
tsconfig.json Normal file
View file

@ -0,0 +1,18 @@
{
"extends": "@manuth/tsconfig/recommended",
"compilerOptions": {
"module": "commonjs",
"lib": [
"es7"
],
"plugins": [
{
"name": "typescript-tslint-plugin"
}
],
"target": "es5"
},
"files": [
"./gulpfile.ts"
]
}

6
tslint.json Normal file
View file

@ -0,0 +1,6 @@
{
"extends": "@manuth/tslint-presets/recommended",
"rules": {
"interface-name": false
}
}