Skip to content

Commit

Permalink
Add WIP version of local packages to WebWriter web
Browse files Browse the repository at this point in the history
  • Loading branch information
salmenf committed Sep 24, 2024
1 parent b65c109 commit 33ad58e
Show file tree
Hide file tree
Showing 13 changed files with 340 additions and 99 deletions.
4 changes: 0 additions & 4 deletions @webwriter/core/model/marshal/html.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { readTextFile, removeFile, writeTextFile } from '@tauri-apps/api/fs'
import {Node, DOMSerializer} from "prosemirror-model"
import { join, appDir } from '@tauri-apps/api/path'
import { EditorState } from 'prosemirror-state'
import {Schema, DOMParser} from "prosemirror-model"

import { createElementWithAttributes, namedNodeMapToObject, unscopePackageName } from "../../utility"
import { EditorStateWithHead, PackageStore, createEditorState, headSchema, headSerializer } from '..'
import { Environment } from '../environment'
import scopedCustomElementRegistry from "@webcomponents/scoped-custom-element-registry/src/scoped-custom-element-registry.js?raw"
Expand Down
8 changes: 7 additions & 1 deletion @webwriter/core/model/schemas/datatypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,13 @@ export class SemVer extends NodeSemVer {
return this.compare(other) === 0
}

toString = () => this.raw; toJSON = () => this.toString()
toString() {
const prerelease = this.prerelease.length? `-${this.prerelease.join(".")}`: ""
const build = this.build.length? `+${this.build.join(".")}`: ""
return `${this.major}.${this.minor}.${this.patch}${prerelease}${build}`
}

toJSON = () => this.toString()
}

export class SemVerRange extends NodeSemVerRange {
Expand Down
2 changes: 1 addition & 1 deletion @webwriter/core/model/schemas/resourceschema/head.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export function deleteHeadElement(headState: EditorState, matcher: Matcher) {
}
})
const resolved = headState.doc.resolve(existingPos!)
const tr = headState.tr.delete(resolved.pos, resolved.pos + existingNode!?.nodeSize ?? 1)
const tr = headState.tr.delete(resolved.pos, resolved.pos + (existingNode!?.nodeSize ?? 1))
return headState.apply(tr)
}

Expand Down
4 changes: 2 additions & 2 deletions @webwriter/core/model/schemas/valuedefinition/index.grammar
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ Some { ("{" Min { number } "}") | "{" Min { number } "," Max { number } "}" }
OneOrMoreCommaSeparated { "#" (ZeroOrMore | OneOrMore | ZeroOrOne | Some)? }

@tokens {
or { "|" }
andor { "||" }
// or { "|" }
// andor { "||" }

Identifier { $[a-zA-Z0-9_\-]+ }
literalString { "," | "." | "(" | ")" | "/" }
Expand Down
10 changes: 5 additions & 5 deletions @webwriter/core/model/stores/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,16 @@ export class RootStore {
}

async persist(schema: ZodSchema<StoreSlice<RootStore>>, settingsPath?: string) {
const {appDir, join} = this.Path
const path = settingsPath ?? await join(await appDir(), "settings.json")
try {
const settings = schema.parse(this)
const contents = JSON.stringify(settings, undefined, 2)
if(this.packages.apiBase) {
localStorage.setItem("webwriter_settings", contents)
if(WEBWRITER_ENVIRONMENT.backend === "tauri") {
const {appDir, join} = this.Path
const path = settingsPath ?? await join(await appDir(), "settings.json")
await this.FS.writeFile(path, contents)
}
else {
await this.FS.writeFile(path, contents)
localStorage.setItem("webwriter_settings", contents)
}
}
catch(cause: any) {
Expand Down
149 changes: 115 additions & 34 deletions @webwriter/core/model/stores/packagestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Options = {
type PmQueueTask = {
command: "install" | "add" | "remove" | "update",
parameters: string[],
handle?: FileSystemDirectoryHandle,
cwd?: string,
name?: string
}
Expand Down Expand Up @@ -71,7 +72,8 @@ export class PackageStore {
importMap: ImportMap

set installedPackages(value: string[]) {
const valueUnique = Array.from(new Set(value))
let valueUnique = Array.from(new Set(value))
valueUnique = valueUnique.filter(a => !valueUnique.some(b => b !== a && b.startsWith("@" + a.split("@")[1])))
localStorage.setItem("webwriter_installedPackages", JSON.stringify(valueUnique))
}

Expand All @@ -97,6 +99,8 @@ export class PackageStore {

apiBase: string

private db = indexedDB.open("webwriter", 1)

private static async readFileIfExists(path: string, FS: Environment["FS"]): Promise<string | undefined> {
return await FS.exists(path)? await FS.readFile(path) as string: undefined
}
Expand Down Expand Up @@ -321,6 +325,26 @@ export class PackageStore {
this.watching = options.watching ?? this.watching
})()
}
else {
this.db.addEventListener("upgradeneeded", () => {
this.db.result.createObjectStore("handles", {keyPath: "id"})
})
}
}

async putLocalHandle(id: string, handle: FileSystemDirectoryHandle) {
const tx = this.db.result.transaction("handles", "readwrite")
const store = tx.objectStore("handles")
const done = new Promise(r => tx.addEventListener("complete", r))
store.put({id, handle})
return done
}

async getLocalHandle(id: string) {
const tx = this.db.result.transaction("handles", "readwrite")
const store = tx.objectStore("handles")
const req = store.get(id)
return new Promise(r => req.addEventListener("success", () => r(req.result.handle)))
}

FS: Environment["FS"]
Expand Down Expand Up @@ -378,8 +402,8 @@ export class PackageStore {
this.issues[id] = [...this.getPackageIssues(id), ...issues]
}

get importingName() {
return Object.keys(this.adding).find(name => this.adding[name] && !(name in this.packages))
get importingId() {
return Object.keys(this.adding).find(id => this.adding[id] && !(id in this.packages))
}

searchIndex = new MiniSearch<Package>({
Expand All @@ -393,19 +417,25 @@ export class PackageStore {
}

async updateImportMap(ids: string[]=this.installedPackages) {
if(!ids.length) {
return
}
const url = new URL("_importmaps", this.apiBase)
url.searchParams.append("pkg", "true")
/*
const nonlocalIds = ids.filter(id => {
const version = id.split("@")[2]
return !(new SemVer(version)).prerelease.includes("local")
})
const localIds = ids.filter(id =>!nonlocalIds.includes(id))
nonlocalIds.forEach(id => url.searchParams.append("id", id))
*/
ids.forEach(id => url.searchParams.append("id", id))
const map = ids.length? await (await fetch(url)).json(): undefined
this.importMap = new ImportMap({map})
this.installedPackages = ids
}

pmQueue = cargoQueue(async (tasks: PmQueueTask[]) => {
const toAdd = tasks.filter(t => t.command === "add" && !t.name).flatMap(t => t.parameters)
const toAdd = tasks.filter(t => t.command === "add" && !t.name && !t.handle).flatMap(t => t.parameters)
const toAddLocal = tasks.filter(t => t.command === "add" && t.handle).flatMap(t => ({handle: t.handle!, name: t.name}))
const toRemove = tasks.filter(t => t.command === "remove").flatMap(t => t.parameters)
const toUpdate = tasks.filter(t => t.command === "update").flatMap(t => t.parameters)
const toLink = tasks.filter(t => t.command === "add" && t.name)
Expand All @@ -430,6 +460,27 @@ export class PackageStore {
this.adding = {...this.adding, ...Object.fromEntries(toAdd.map(name => [names[name], false]))}
}
}
if(toAddLocal.length > 0) {
try {
const pkgs = await Promise.all(toAddLocal.map(async ({handle, name}) => {
const pkgHandle = await handle.getFileHandle("package.json")
const file = await pkgHandle.getFile()
const text = await file.text()
const pkg = new Package({...JSON.parse(text)})
pkg.version.prerelease = [...pkg.version.prerelease, "local"]
await this.putLocalHandle(pkg.id, handle)
return pkg
}))
this.updateImportMap([...this.installedPackages, ...pkgs.map(pkg => pkg.id)])
}
catch(err) {
console.error(err)
}
finally {
await this.load()
this.adding = {...this.adding, ...Object.fromEntries(toAddLocal.map(({name}) => [name, false]))}
}
}
if(toLink.length > 0) {
const pkg = (await this.readRootPackageJson())!
pkg.localPaths = {
Expand Down Expand Up @@ -576,9 +627,17 @@ export class PackageStore {
return this.FS.writeFile(await this.rootPackageJsonPath, JSON.stringify(pkg, undefined, 2))
}

async add(url: string, name?: string) {
this.adding = {...this.adding, [name ?? url]: true}
return this.pmQueue.push({command: "add", parameters: [url], cwd: await this.appDir, name})
async add(urlOrHandle: string | FileSystemDirectoryHandle, name?: string) {
if(typeof urlOrHandle === "string") {
const url = urlOrHandle
this.adding = {...this.adding, [name ?? url]: true}
return this.pmQueue.push({command: "add", parameters: [url], cwd: await this.appDir, name})
}
else {
const handle = urlOrHandle
this.adding = {...this.adding, [name ?? handle.name]: true}
return this.pmQueue.push({command: "add", parameters: [], handle, cwd: await this.appDir, name})
}
}

async remove(name: string) {
Expand All @@ -589,7 +648,7 @@ export class PackageStore {
async update(name?: string) {
this.updating = name
? {...this.updating, [name]: true}
: Object.fromEntries(this.packagesList.filter(pkg => pkg.installed).map(pkg => [pkg.name, true]))
: Object.fromEntries(this.packagesList.filter(pkg => pkg.installed).map(pkg => [pkg.id, true]))
return this.pmQueue.push({command: "update", parameters: name? [name, "--latest"]: ["--latest"], cwd: await this.appDir})
}

Expand Down Expand Up @@ -676,22 +735,30 @@ export class PackageStore {
}
}
for(const pkg of installed) {
const aPkg = available.find(a => a.name === pkg.name)
const aPkg = available.find(a => a.id === pkg.id)
if(aPkg) {
available = available.filter(a => a.name !== pkg.name)
available = available.filter(a => a.name !== pkg.id)
}
const latest = aPkg?.version
final.push(pkg.extend({latest, installed: true}))
}
final = final.concat(available)
this.packages = Object.fromEntries(final.map(pkg => [pkg.name, pkg]))
this.packages = Object.fromEntries(final.map(pkg => [pkg.id, pkg]))
}
else {
final = available.map(pkg => pkg.extend({installed: this.installedPackages.includes(pkg.name)})).sort((a, b) => Number(!!b.installed) - Number(!!a.installed))
const localIds = this.installedPackages.filter(id => !available.some(pkg => pkg.id === id))
const local = (await Promise.all(localIds.map(id => fetch(new URL(id + "/package.json", this.apiBase)).then(resp => resp.json()))))
.map(json => {
const version = new SemVer(json.version)
version.prerelease = [...version.prerelease, "local"]
return new Package({...json, installed: true, localPath: "", version})
})
console.log(local)
final = available.map(pkg => pkg.extend({installed: this.installedPackages.includes(pkg.id)})).sort((a, b) => Number(!!b.installed) - Number(!!a.installed))
await this.updateImportMap()
this.bundleID = PackageStore.computeBundleID(this.installedPackages, false);
(this.onBundleChange ?? (() => null))(final.filter(pkg => pkg.installed))
this.packages = Object.fromEntries(final.map(pkg => [pkg.name, pkg]))
this.packages = Object.fromEntries(final.map(pkg => [pkg.id, pkg]))
}
this.searchIndex.removeAll()
this.searchIndex.addAll(final)
Expand Down Expand Up @@ -771,7 +838,7 @@ export class PackageStore {
// this.appendPackageIssues(id, cause as Error)
}
const members = await this.readPackageMembers(pkgJson, pkgRootPath)
return new Package(pkgJson, {installed: true, watching: this.watching[pkgJson.name], localPath, members, lastLoaded: Date.now()})
return new Package(pkgJson, {installed: true, watching: this.watching[pkgJson.id], localPath, members, lastLoaded: Date.now()})
}

private async readPackageMembers(pkg: Package, path?: string) {
Expand Down Expand Up @@ -860,8 +927,8 @@ export class PackageStore {



getPackageMembers(name: string, filter?: "widgets" | "snippets" | "themes") {
const pkg = this.packages[name]
getPackageMembers(id: string, filter?: "widgets" | "snippets" | "themes") {
const pkg = this.packages[id]
const members = {} as any
for(const [memberName, member] of Object.entries(pkg?.members ?? {})) {
const is = {
Expand All @@ -878,18 +945,18 @@ export class PackageStore {
}

get widgets() {
return Object.fromEntries(Object.keys(this.packages).map(name => [name, this.getPackageMembers(name, "widgets")]))
return Object.fromEntries(Object.entries(this.members).map(([k, v]) => [k, filterObject(v, vk => vk.startsWith("./widgets/"))]))
return Object.fromEntries(Object.keys(this.packages).map(id => [id, this.getPackageMembers(id, "widgets")]))
// return Object.fromEntries(Object.entries(this.members).map(([k, v]) => [k, filterObject(v, vk => vk.startsWith("./widgets/"))]))
}

get snippets() {
return Object.fromEntries(Object.keys(this.packages).map(name => [name, this.getPackageMembers(name, "snippets")]))
return Object.fromEntries(Object.entries(this.members).map(([k, v]) => [k, filterObject(v, vk => vk.startsWith("./snippets/"))]))
return Object.fromEntries(Object.keys(this.packages).map(id => [id, this.getPackageMembers(id, "snippets")]))
// return Object.fromEntries(Object.entries(this.members).map(([k, v]) => [k, filterObject(v, vk => vk.startsWith("./snippets/"))]))
}

get themes() {
return Object.fromEntries(Object.keys(this.packages).map(name => [name, this.getPackageMembers(name, "themes")]))
return Object.fromEntries(Object.entries(this.members).map(([k, v]) => [k, filterObject(v, vk => vk.startsWith("./themes/"))]))
return Object.fromEntries(Object.keys(this.packages).map(id => [id, this.getPackageMembers(id, "themes")]))
// return Object.fromEntries(Object.entries(this.members).map(([k, v]) => [k, filterObject(v, vk => vk.startsWith("./themes/"))]))
}

get allThemes() {
Expand All @@ -903,7 +970,7 @@ export class PackageStore {
}

get installedWidgetUrls() {
return this.apiBase? this.installed.flatMap(pkg => Object.keys(pkg?.widgets ?? {}).flatMap(k => new URL(pkg.name + k.slice(1), this.apiBase).href)): []
return this.apiBase? this.installed.flatMap(pkg => Object.keys(pkg?.widgets ?? {}).flatMap(k => new URL(pkg.id + k.slice(1), this.apiBase).href)): []
}

get widgetTagNames() {
Expand Down Expand Up @@ -959,16 +1026,30 @@ export class PackageStore {
}

/** Reads a local package directory, returning the package config. */
async readLocal(path: string) {
const resolvedPath = await this.Path.resolve(path)
const pkgJsonPath = await this.Path.join(resolvedPath, "package.json")
const exists = await this.FS.exists(pkgJsonPath)
if(!exists) {
throw Error("No package found under " + pkgJsonPath)
async readLocal(pathOrHandle: string | FileSystemDirectoryHandle) {
let pkgString: string
if(typeof pathOrHandle === "string") {
const resolvedPath = await this.Path.resolve(pathOrHandle)
const pkgJsonPath = await this.Path.join(resolvedPath, "package.json")
const exists = await this.FS.exists(pkgJsonPath)
if(!exists) {
throw Error("No package found under " + pkgJsonPath)
}
pkgString = await this.FS.readFile(pkgJsonPath) as string
}
else {
let pkgJsonHandle
try {
pkgJsonHandle = await pathOrHandle.getFileHandle("package.json")
}
catch(err) {
throw Error("No package found under " + pathOrHandle.name)
}
const file = await pkgJsonHandle.getFile()
pkgString = await file.text()
}
const pkgString = await this.FS.readFile(pkgJsonPath) as string
try {
return new Package(JSON.parse(pkgString))
return new Package(JSON.parse(pkgString!))
}
catch(cause) {
throw new PackageJsonIssue(`Error parsing package.json: ${cause}`, {cause})
Expand Down
14 changes: 7 additions & 7 deletions @webwriter/core/view/editor/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,14 @@ export class ExplorableEditor extends LitElement {
return range(n).map(k => `ww_${(floor + k).toString(36)}`)
}

insertMember = async (pkgID: string, insertableName: string) => {
insertMember = async (id: string, insertableName: string) => {
const state = this.pmEditor.state
const name = (pkgID.startsWith("@")? "@": "") + pkgID.split("@")[pkgID.startsWith("@")? 1: 0]
console.log(name)
const members = this.app.store.packages.getPackageMembers(name)
const members = this.app.store.packages.getPackageMembers(id)
if(insertableName.startsWith("./snippets/")) {
const source = members[insertableName].source
let htmlStr = source
if(!source) {
const url = this.app.store.packages.importMap.resolve("@" + pkgID.split("@").slice(0, 2).join("") + insertableName.slice(1) + ".html")
const url = this.app.store.packages.importMap.resolve("@" + id.split("@").slice(0, 2).join("") + insertableName.slice(1) + ".html")
htmlStr = await (await fetch(url, {headers: {"Accept": "text/html"}})).text()
}
const tagNames = this.app.store.packages.widgetTagNames
Expand Down Expand Up @@ -143,7 +141,7 @@ export class ExplorableEditor extends LitElement {
}
else if(insertableName.startsWith("./themes/")) {
const old = this.app.store.document.themeName
const toInsert = pkgID + insertableName.slice(1)
const toInsert = id + insertableName.slice(1)
const value = old === toInsert? "base": toInsert
const allThemes = this.app.store.packages.allThemes as any
this.app.store.document.setHead(upsertHeadElement(
Expand Down Expand Up @@ -1298,7 +1296,9 @@ export class ExplorableEditor extends LitElement {
@ww-watch-widget=${async (e: CustomEvent) => {
const name = e.detail.name
await this.app.store.packages.toggleWatch(name)
this.app.settings.setAndPersist("packages", "watching", this.app.store.packages.watching)
if(WEBWRITER_ENVIRONMENT.backend === "tauri") {
this.app.settings.setAndPersist("packages", "watching", this.app.store.packages.watching)
}
}}
.packages=${this.packages}
tabindex="-1"
Expand Down
Loading

0 comments on commit 33ad58e

Please sign in to comment.