Merge pull request #448 from vector-im/snowpack-mvp

Snowpack MVP
This commit is contained in:
Bruno Windels 2021-08-18 13:09:02 +00:00 committed by GitHub
commit 02e422f3ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 2453 additions and 64 deletions

View file

@ -10,7 +10,7 @@
"lint": "eslint --cache src/", "lint": "eslint --cache src/",
"lint-ci": "eslint src/", "lint-ci": "eslint src/",
"test": "impunity --entry-point src/main.js --force-esm-dirs lib/ src/", "test": "impunity --entry-point src/main.js --force-esm-dirs lib/ src/",
"start": "node scripts/serve-local.js", "start": "snowpack dev --port 3000",
"build": "node --experimental-modules scripts/build.mjs", "build": "node --experimental-modules scripts/build.mjs",
"postinstall": "node ./scripts/post-install.js" "postinstall": "node ./scripts/post-install.js"
}, },
@ -39,7 +39,7 @@
"eslint": "^7.25.0", "eslint": "^7.25.0",
"fake-indexeddb": "^3.1.2", "fake-indexeddb": "^3.1.2",
"finalhandler": "^1.1.1", "finalhandler": "^1.1.1",
"impunity": "^1.0.0", "impunity": "^1.0.1",
"mdn-polyfills": "^5.20.0", "mdn-polyfills": "^5.20.0",
"node-html-parser": "^4.0.0", "node-html-parser": "^4.0.0",
"postcss": "^8.1.1", "postcss": "^8.1.1",
@ -51,6 +51,8 @@
"rollup": "^2.26.4", "rollup": "^2.26.4",
"rollup-plugin-cleanup": "^3.1.1", "rollup-plugin-cleanup": "^3.1.1",
"serve-static": "^1.13.2", "serve-static": "^1.13.2",
"snowpack": "^3.8.3",
"typescript": "^4.3.5",
"xxhashjs": "^0.2.2" "xxhashjs": "^0.2.2"
}, },
"dependencies": { "dependencies": {

View file

@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {build as snowpackBuild, loadConfiguration} from "snowpack"
import cheerio from "cheerio"; import cheerio from "cheerio";
import fsRoot from "fs"; import fsRoot from "fs";
const fs = fsRoot.promises; const fs = fsRoot.promises;
@ -45,8 +46,11 @@ import flexbugsFixes from "postcss-flexbugs-fixes";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const projectDir = path.join(__dirname, "../"); const projectDir = path.join(__dirname, "../");
const snowpackOutPath = path.join(projectDir, "snowpack-build-output");
const cssSrcDir = path.join(projectDir, "src/platform/web/ui/css/"); const cssSrcDir = path.join(projectDir, "src/platform/web/ui/css/");
const srcDir = path.join(projectDir, "src/"); const snowpackConfig = await loadConfiguration({buildOptions: {out: snowpackOutPath}}, "snowpack.config.js");
const snowpackOutDir = snowpackConfig.buildOptions.out.substring(projectDir.length);
const srcDir = path.join(projectDir, `${snowpackOutDir}/src/`);
const isPathInSrcDir = path => path.startsWith(srcDir); const isPathInSrcDir = path => path.startsWith(srcDir);
const parameters = new commander.Command(); const parameters = new commander.Command();
@ -56,14 +60,27 @@ parameters
.option("--override-css <main css file>", "pass in an alternative main css file") .option("--override-css <main css file>", "pass in an alternative main css file")
parameters.parse(process.argv); parameters.parse(process.argv);
/**
* We use Snowpack to handle the translation of TypeScript
* into JavaScript. We thus can't bundle files straight from
* the src directory, since some of them are TypeScript, and since
* they may import Node modules. We thus bundle files after they
* have been processed by Snowpack. This function returns paths
* to the files that have already been pre-processed in this manner.
*/
function srcPath(src) {
return path.join(snowpackOutDir, 'src', src);
}
async function build({modernOnly, overrideImports, overrideCss}) { async function build({modernOnly, overrideImports, overrideCss}) {
await snowpackBuild({config: snowpackConfig});
// get version number // get version number
const version = JSON.parse(await fs.readFile(path.join(projectDir, "package.json"), "utf8")).version; const version = JSON.parse(await fs.readFile(path.join(projectDir, "package.json"), "utf8")).version;
let importOverridesMap; let importOverridesMap;
if (overrideImports) { if (overrideImports) {
importOverridesMap = await readImportOverrides(overrideImports); importOverridesMap = await readImportOverrides(overrideImports);
} }
const devHtml = await fs.readFile(path.join(projectDir, "index.html"), "utf8"); const devHtml = await fs.readFile(path.join(snowpackOutPath, "index.html"), "utf8");
const doc = cheerio.load(devHtml); const doc = cheerio.load(devHtml);
const themes = []; const themes = [];
findThemes(doc, themeName => { findThemes(doc, themeName => {
@ -77,13 +94,13 @@ async function build({modernOnly, overrideImports, overrideCss}) {
// copy olm assets // copy olm assets
const olmAssets = await copyFolder(path.join(projectDir, "lib/olm/"), assets.directory); const olmAssets = await copyFolder(path.join(projectDir, "lib/olm/"), assets.directory);
assets.addSubMap(olmAssets); assets.addSubMap(olmAssets);
await assets.write(`hydrogen.js`, await buildJs("src/main.js", ["src/platform/web/Platform.js"], importOverridesMap)); await assets.write(`hydrogen.js`, await buildJs(srcPath("main.js"), [srcPath("platform/web/Platform.js")], importOverridesMap));
if (!modernOnly) { if (!modernOnly) {
await assets.write(`hydrogen-legacy.js`, await buildJsLegacy("src/main.js", [ await assets.write(`hydrogen-legacy.js`, await buildJsLegacy(srcPath("main.js"), [
'src/platform/web/legacy-polyfill.js', srcPath('platform/web/legacy-polyfill.js'),
'src/platform/web/LegacyPlatform.js' srcPath('platform/web/LegacyPlatform.js')
], importOverridesMap)); ], importOverridesMap));
await assets.write(`worker.js`, await buildJsLegacy("src/platform/web/worker/main.js", ['src/platform/web/worker/polyfill.js'])); await assets.write(`worker.js`, await buildJsLegacy(srcPath("platform/web/worker/main.js"), [srcPath('platform/web/worker/polyfill.js')]));
} }
// copy over non-theme assets // copy over non-theme assets
const baseConfig = JSON.parse(await fs.readFile(path.join(projectDir, "assets/config.json"), {encoding: "utf8"})); const baseConfig = JSON.parse(await fs.readFile(path.join(projectDir, "assets/config.json"), {encoding: "utf8"}));
@ -97,13 +114,14 @@ async function build({modernOnly, overrideImports, overrideCss}) {
await buildManifest(assets); await buildManifest(assets);
// all assets have been added, create a hash from all assets name to cache unhashed files like index.html // all assets have been added, create a hash from all assets name to cache unhashed files like index.html
assets.addToHashForAll("index.html", devHtml); assets.addToHashForAll("index.html", devHtml);
let swSource = await fs.readFile(path.join(projectDir, "src/platform/web/service-worker.js"), "utf8"); let swSource = await fs.readFile(path.join(snowpackOutPath, "service-worker.js"), "utf8");
assets.addToHashForAll("sw.js", swSource); assets.addToHashForAll("service-worker.js", swSource);
const globalHash = assets.hashForAll(); const globalHash = assets.hashForAll();
await buildServiceWorker(swSource, version, globalHash, assets); await buildServiceWorker(swSource, version, globalHash, assets);
await buildHtml(doc, version, baseConfig, globalHash, modernOnly, assets); await buildHtml(doc, version, baseConfig, globalHash, modernOnly, assets);
await removeDirIfExists(snowpackOutPath);
console.log(`built hydrogen ${version} (${globalHash}) successfully with ${assets.size} files`); console.log(`built hydrogen ${version} (${globalHash}) successfully with ${assets.size} files`);
} }
@ -156,7 +174,7 @@ async function buildHtml(doc, version, baseConfig, globalHash, modernOnly, asset
const configJSON = JSON.stringify(Object.assign({}, baseConfig, { const configJSON = JSON.stringify(Object.assign({}, baseConfig, {
worker: assets.has("worker.js") ? assets.resolve(`worker.js`) : null, worker: assets.has("worker.js") ? assets.resolve(`worker.js`) : null,
downloadSandbox: assets.resolve("download-sandbox.html"), downloadSandbox: assets.resolve("download-sandbox.html"),
serviceWorker: "sw.js", serviceWorker: "service-worker.js",
olm: { olm: {
wasm: assets.resolve("olm.wasm"), wasm: assets.resolve("olm.wasm"),
legacyBundle: assets.resolve("olm_legacy.js"), legacyBundle: assets.resolve("olm_legacy.js"),
@ -324,7 +342,7 @@ async function buildServiceWorker(swSource, version, globalHash, assets) {
swSource = replaceStringInSource("NOTIFICATION_BADGE_ICON", assets.resolve("icon.png")); swSource = replaceStringInSource("NOTIFICATION_BADGE_ICON", assets.resolve("icon.png"));
// service worker should not have a hashed name as it is polled by the browser for updates // service worker should not have a hashed name as it is polled by the browser for updates
await assets.writeUnhashed("sw.js", swSource); await assets.writeUnhashed("service-worker.js", swSource);
} }
async function buildCssBundles(buildFn, themes, assets, mainCssFile = null) { async function buildCssBundles(buildFn, themes, assets, mainCssFile = null) {

View file

@ -75,7 +75,7 @@ async function populateLib() {
const olmDstDir = path.join(libDir, "olm/"); const olmDstDir = path.join(libDir, "olm/");
await fs.mkdir(olmDstDir); await fs.mkdir(olmDstDir);
for (const file of ["olm.js", "olm.wasm", "olm_legacy.js"]) { for (const file of ["olm.js", "olm.wasm", "olm_legacy.js"]) {
await fs.symlink(path.join(olmSrcDir, file), path.join(olmDstDir, file)); await fs.copyFile(path.join(olmSrcDir, file), path.join(olmDstDir, file));
} }
// transpile node-html-parser to esm // transpile node-html-parser to esm
await fs.mkdir(path.join(libDir, "node-html-parser/")); await fs.mkdir(path.join(libDir, "node-html-parser/"));
@ -83,12 +83,6 @@ async function populateLib() {
require.resolve('node-html-parser/dist/index.js'), require.resolve('node-html-parser/dist/index.js'),
path.join(libDir, "node-html-parser/index.js") path.join(libDir, "node-html-parser/index.js")
); );
// Symlink dompurify
await fs.mkdir(path.join(libDir, "dompurify/"));
await fs.symlink(
require.resolve('dompurify/dist/purify.es.js'),
path.join(libDir, "dompurify/index.js")
);
// transpile another-json to esm // transpile another-json to esm
await fs.mkdir(path.join(libDir, "another-json/")); await fs.mkdir(path.join(libDir, "another-json/"));
await commonjsToESM( await commonjsToESM(

38
snowpack.config.js Normal file
View file

@ -0,0 +1,38 @@
// Snowpack Configuration File
// See all supported options: https://www.snowpack.dev/reference/configuration
/** @type {import("snowpack").SnowpackUserConfig } */
module.exports = {
mount: {
// More specific paths before less specific paths (if they overlap)
"src/platform/web/docroot": "/",
"src": "/src",
"lib": {url: "/lib", static: true },
"assets": "/assets",
/* ... */
},
exclude: [
/* Avoid scanning scripts which use dev-dependencies and pull in babel, rollup, etc. */
'**/node_modules/**/*',
'**/scripts/**',
'**/target/**',
'**/prototypes/**',
'**/src/matrix/storage/memory/**',
'**/src/platform/web/legacy-polyfill.js',
'**/src/platform/web/worker/polyfill.js'
],
plugins: [
/* ... */
],
packageOptions: {
/* ... */
},
devOptions: {
open: "none",
hmr: false,
/* ... */
},
buildOptions: {
/* ... */
},
};

View file

@ -18,20 +18,26 @@ limitations under the License.
import {EventKey} from "../EventKey.js"; import {EventKey} from "../EventKey.js";
export const PENDING_FRAGMENT_ID = Number.MAX_SAFE_INTEGER; export const PENDING_FRAGMENT_ID = Number.MAX_SAFE_INTEGER;
interface FragmentIdComparer {
compare: (a: number, b: number) => number
}
export class BaseEntry { export class BaseEntry {
constructor(fragmentIdComparer) { protected _fragmentIdComparer: FragmentIdComparer
constructor(fragmentIdComparer: FragmentIdComparer) {
this._fragmentIdComparer = fragmentIdComparer; this._fragmentIdComparer = fragmentIdComparer;
} }
get fragmentId() { get fragmentId(): number {
throw new Error("unimplemented"); throw new Error("unimplemented");
} }
get entryIndex() { get entryIndex(): number {
throw new Error("unimplemented"); throw new Error("unimplemented");
} }
compare(otherEntry) { compare(otherEntry: BaseEntry): number {
if (this.fragmentId === otherEntry.fragmentId) { if (this.fragmentId === otherEntry.fragmentId) {
return this.entryIndex - otherEntry.entryIndex; return this.entryIndex - otherEntry.entryIndex;
} else if (this.fragmentId === PENDING_FRAGMENT_ID) { } else if (this.fragmentId === PENDING_FRAGMENT_ID) {
@ -44,9 +50,9 @@ export class BaseEntry {
} }
} }
asEventKey() { asEventKey(): EventKey {
return new EventKey(this.fragmentId, this.entryIndex); return new EventKey(this.fragmentId, this.entryIndex);
} }
updateFrom() {} updateFrom(other: BaseEntry) {}
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {BaseEntry} from "./BaseEntry.js"; import {BaseEntry} from "./BaseEntry";
import {REDACTION_TYPE} from "../../common.js"; import {REDACTION_TYPE} from "../../common.js";
import {createAnnotation, ANNOTATION_RELATION_TYPE, getRelationFromContent} from "../relations.js"; import {createAnnotation, ANNOTATION_RELATION_TYPE, getRelationFromContent} from "../relations.js";
import {PendingAnnotation} from "../PendingAnnotation.js"; import {PendingAnnotation} from "../PendingAnnotation.js";

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {BaseEntry} from "./BaseEntry.js"; import {BaseEntry} from "./BaseEntry";
import {Direction} from "../Direction.js"; import {Direction} from "../Direction.js";
import {isValidFragmentId} from "../common.js"; import {isValidFragmentId} from "../common.js";
import {KeyLimits} from "../../../storage/common.js"; import {KeyLimits} from "../../../storage/common.js";

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {PENDING_FRAGMENT_ID} from "./BaseEntry.js"; import {PENDING_FRAGMENT_ID} from "./BaseEntry";
import {BaseEventEntry} from "./BaseEventEntry.js"; import {BaseEventEntry} from "./BaseEventEntry.js";
export class PendingEventEntry extends BaseEventEntry { export class PendingEventEntry extends BaseEventEntry {

View file

@ -26,7 +26,7 @@
downloadSandbox: "assets/download-sandbox.html", downloadSandbox: "assets/download-sandbox.html",
defaultHomeServer: "matrix.org", defaultHomeServer: "matrix.org",
// NOTE: uncomment this if you want the service worker for local development // NOTE: uncomment this if you want the service worker for local development
// serviceWorker: "sw.js", // serviceWorker: "service-worker.js",
// NOTE: provide push config if you want push notifs for local development // NOTE: provide push config if you want push notifs for local development
// see assets/config.json for what the config looks like // see assets/config.json for what the config looks like
// push: {...}, // push: {...},

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import DOMPurify from "../../../lib/dompurify/index.js" import DOMPurify from "dompurify"
class HTMLParseResult { class HTMLParseResult {
constructor(bodyNode) { constructor(bodyNode) {

1
sw.js
View file

@ -1 +0,0 @@
src/platform/web/service-worker.js

8
tsconfig.json Normal file
View file

@ -0,0 +1,8 @@
{
"compilerOptions": {
"strictNullChecks": true,
"noEmit": true,
"target": "es6"
},
"include": ["src/**/*"],
}

2388
yarn.lock

File diff suppressed because it is too large Load diff