@ape-egg/codecane 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -2
- package/core/build.js +66 -0
- package/core/check.js +37 -0
- package/core/checks.js +94 -0
- package/core/cli.js +111 -0
- package/core/data.js +64 -0
- package/core/diff.js +43 -0
- package/core/index.js +10 -0
- package/core/modules.js +17 -0
- package/core/patch.js +115 -0
- package/core/rebase.js +80 -0
- package/core/serve.js +170 -0
- package/core/stack.js +199 -0
- package/core/steps.js +102 -0
- package/package.json +31 -4
- package/ui/codecane.css +554 -0
- package/ui/components/Changes.html +25 -0
- package/ui/components/Stage.html +40 -0
- package/ui/components/Story.html +105 -0
- package/ui/components/Timeline.html +25 -0
- package/ui/index.html +20 -0
- package/ui/mounted.js +73 -0
- package/ui/stylecheat.css +8473 -0
- package/ui/viewer.js +295 -0
- package/workflow.md +81 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<story-panel ontransitionend="event.target === this && codecane.returned()">
|
|
2
|
+
<panel-tabs stylecheat dark>
|
|
3
|
+
<tabs primary>
|
|
4
|
+
<tab active="@[tab === 'about']" onclick="$.tab = 'about'">About</tab>
|
|
5
|
+
<tab active="@[tab === 'files']" onclick="$.tab = 'files'">Files</tab>
|
|
6
|
+
</tabs>
|
|
7
|
+
</panel-tabs>
|
|
8
|
+
<!-- if tab === 'about' -->
|
|
9
|
+
<article>
|
|
10
|
+
<step-status stylecheat dark>
|
|
11
|
+
<crow justify-between>
|
|
12
|
+
<small>Step @[timeline.indexOf(page.id) + 1] of @[timeline.length]</small>
|
|
13
|
+
<!-- if page.untested -->
|
|
14
|
+
<badge bg-slate text-slate>Untested</badge>
|
|
15
|
+
<!-- /if -->
|
|
16
|
+
</crow>
|
|
17
|
+
</step-status>
|
|
18
|
+
<!-- if page.convergence && page.convergence.kind === 'seam' -->
|
|
19
|
+
<story><h1>@[page.title]</h1></story>
|
|
20
|
+
<!-- else -->
|
|
21
|
+
<story>@[$.unsafe(page.story)]</story>
|
|
22
|
+
<!-- /if -->
|
|
23
|
+
<!-- if page.convergence -->
|
|
24
|
+
<convergence kind="@[page.convergence.kind]">
|
|
25
|
+
<h2>@[kinds[page.convergence.kind]]</h2>
|
|
26
|
+
<!-- if page.convergence.kind === 'seam' -->
|
|
27
|
+
<p>A seam is a small API that the timelines before it share. Each timeline fills it in where it owns the code, so the steps after it are written once and never ask which timeline they are on.</p>
|
|
28
|
+
<!-- if page.convergence.requiredBy.length -->
|
|
29
|
+
<p>Required by @[page.convergence.requiredBy.map(id => steps[id].title).join(', ')]:</p>
|
|
30
|
+
<ul>
|
|
31
|
+
<!-- each page.convergence.reasons as reason -->
|
|
32
|
+
<li>@[reason]</li>
|
|
33
|
+
<!-- /each -->
|
|
34
|
+
</ul>
|
|
35
|
+
<!-- /if -->
|
|
36
|
+
<!-- else -->
|
|
37
|
+
<!-- if page.convergence.continues.length -->
|
|
38
|
+
<p>Continues @[page.convergence.continues.map(id => steps[id].title).join(', ')] together, each timeline on its own.</p>
|
|
39
|
+
<!-- /if -->
|
|
40
|
+
<!-- if page.convergence.brought.length -->
|
|
41
|
+
<p>Brings @[page.convergence.brought.map(id => steps[id].title).join(', ')] onto this timeline.</p>
|
|
42
|
+
<!-- /if -->
|
|
43
|
+
<!-- if page.convergence.kind === 'merge' -->
|
|
44
|
+
<p>Every step replays cleanly and every check passes, so the timelines just join.</p>
|
|
45
|
+
<!-- /if -->
|
|
46
|
+
<!-- /if -->
|
|
47
|
+
</convergence>
|
|
48
|
+
<!-- /if -->
|
|
49
|
+
<!-- if page.convergence && page.convergence.kind === 'seam' -->
|
|
50
|
+
<story>@[$.unsafe(page.story)]</story>
|
|
51
|
+
<!-- /if -->
|
|
52
|
+
<!-- if alternatives.length -->
|
|
53
|
+
<alternatives>
|
|
54
|
+
<h2>Alternatives</h2>
|
|
55
|
+
<!-- each alternatives as alternative -->
|
|
56
|
+
<a href="/@[alternative.address]">@[alternative.title]</a>
|
|
57
|
+
<!-- /each -->
|
|
58
|
+
</alternatives>
|
|
59
|
+
<!-- /if -->
|
|
60
|
+
</article>
|
|
61
|
+
<!-- else -->
|
|
62
|
+
<file-list stylecheat dark>
|
|
63
|
+
<!-- each [...new Set(page.files.map(file => file.folder))] as folder (page.id + folder) -->
|
|
64
|
+
<!-- if folder -->
|
|
65
|
+
<folder>@[folder]</folder>
|
|
66
|
+
<!-- /if -->
|
|
67
|
+
<!-- each page.files.filter(file => file.folder === folder) as file (page.id + file.path) -->
|
|
68
|
+
<file data-path="@[file.path]" aria-current="@[file.path === open ? 'location' : 'false']" onclick="codecane.open('@[file.path]')" onmouseenter="codecane.light('@[file.changedBy]')" onmouseleave="codecane.light()">
|
|
69
|
+
<span>@[file.path.slice(folder.length)]</span>
|
|
70
|
+
<!-- if file.owner === page.id && file.overrides -->
|
|
71
|
+
<badge bg-orange text-orange>Override</badge>
|
|
72
|
+
<!-- /if -->
|
|
73
|
+
<!-- if file.owner === page.id && !file.overrides -->
|
|
74
|
+
<badge bg-green text-green>Added</badge>
|
|
75
|
+
<!-- /if -->
|
|
76
|
+
<!-- if file.patchedBy.includes(page.id) -->
|
|
77
|
+
<badge bg-yellow text-yellow>Modified</badge>
|
|
78
|
+
<!-- /if -->
|
|
79
|
+
</file>
|
|
80
|
+
<!-- /each -->
|
|
81
|
+
<!-- /each -->
|
|
82
|
+
<!-- if page.dependencies.length -->
|
|
83
|
+
<folder>node_modules/</folder>
|
|
84
|
+
<!-- each page.dependencies as dependency (page.id + dependency) -->
|
|
85
|
+
<dependency>@[dependency]</dependency>
|
|
86
|
+
<!-- /each -->
|
|
87
|
+
<!-- /if -->
|
|
88
|
+
</file-list>
|
|
89
|
+
<!-- /if -->
|
|
90
|
+
<footer>
|
|
91
|
+
<!-- if page.ahead -->
|
|
92
|
+
<story>@[$.unsafe(page.ahead)]</story>
|
|
93
|
+
<!-- /if -->
|
|
94
|
+
<step-links>
|
|
95
|
+
<!-- if previous -->
|
|
96
|
+
<a href="/@[previous.address]" kind="@[previous.kind]">← @[previous.title]</a>
|
|
97
|
+
<!-- /if -->
|
|
98
|
+
<!-- if next -->
|
|
99
|
+
<a href="/@[next.address]" next kind="@[next.kind]">@[next.title] →</a>
|
|
100
|
+
<!-- else -->
|
|
101
|
+
<span>Final build</span>
|
|
102
|
+
<!-- /if -->
|
|
103
|
+
</step-links>
|
|
104
|
+
</footer>
|
|
105
|
+
</story-panel>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<header @[fold] stylecheat dark away="@[fullscreen && !returning]">
|
|
2
|
+
<a href="/" aria-current="@[root ? 'page' : 'false']"><strong>@[name]</strong></a>
|
|
3
|
+
<ol style="grid-template-rows: @[rows]">
|
|
4
|
+
<!-- each cells as cell (cell.key) -->
|
|
5
|
+
<li style="grid-area: @[cell.lane + 1] / @[cell.depth + 1] / span @[cell.span]" @[cell.shown]>
|
|
6
|
+
<!-- if cell.follows -->
|
|
7
|
+
<arrow>→</arrow>
|
|
8
|
+
<!-- /if -->
|
|
9
|
+
<a href="/@[cell.address]" onclick="codecane.pick(event, '@[cell.id]', @[cell.lane], @[cell.span])" aria-current="@[page.id === cell.id ? 'step' : 'false']" kind="@[steps[cell.id].kind ?? '']" @[cell.lit]>
|
|
10
|
+
<!-- if steps[cell.id].kind -->
|
|
11
|
+
<join aria-hidden="true">Y</join>
|
|
12
|
+
<!-- /if -->
|
|
13
|
+
<!-- if steps[cell.id].kind !== 'seam' -->
|
|
14
|
+
@[steps[cell.id].title]
|
|
15
|
+
<!-- /if -->
|
|
16
|
+
</a>
|
|
17
|
+
<!-- if cell.forks -->
|
|
18
|
+
<button fork title="@[cell.forks] timelines fork after @[steps[cell.id].title]" aria-expanded="@[fold && through === cell.id ? 'true' : 'false']" onclick="codecane.fold('@[cell.id]')"><split aria-hidden="true">Y</split>@[cell.forks]</button>
|
|
19
|
+
<!-- /if -->
|
|
20
|
+
</li>
|
|
21
|
+
<!-- /each -->
|
|
22
|
+
</ol>
|
|
23
|
+
<logotext-codecane><a href="https://codecane.korte.kim" target="_blank">code<span>cane</span></a><version>v@[version]</version></logotext-codecane>
|
|
24
|
+
<button onclick="codecane.expand()" aria-label="Fullscreen" tertiary sm><icon fullscreen></icon></button>
|
|
25
|
+
</header>
|
package/ui/index.html
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>codecane</title>
|
|
7
|
+
<link rel="stylesheet" href="/node_modules/@ape-egg/codie/codie.css">
|
|
8
|
+
<link rel="stylesheet" href="/codecane/ui/stylecheat.css">
|
|
9
|
+
<link rel="stylesheet" href="/codecane/ui/codecane.css">
|
|
10
|
+
<script type="module" src="/codecane/ui/viewer.js"></script>
|
|
11
|
+
</head>
|
|
12
|
+
<body vibe-fouc>
|
|
13
|
+
<component src="/codecane/ui/components/Timeline.html"></component>
|
|
14
|
+
<workspace fullscreen="@[fullscreen]" away="@[fullscreen && !returning]">
|
|
15
|
+
<component src="/codecane/ui/components/Story.html"></component>
|
|
16
|
+
<component src="/codecane/ui/components/Stage.html"></component>
|
|
17
|
+
<component src="/codecane/ui/components/Changes.html"></component>
|
|
18
|
+
</workspace>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
package/ui/mounted.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const blobs = {}
|
|
2
|
+
const sources = {}
|
|
3
|
+
const blob = (text, type, path) => {
|
|
4
|
+
const url = blobs[`${path}\n${text}`] ??= URL.createObjectURL(new Blob([text], { type }))
|
|
5
|
+
sources[url] = path
|
|
6
|
+
return url
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const moduleLinks = /((?:from|import)\s*\(?\s*)(['"])(\.{1,2}\/[^'"]+)\2/g
|
|
10
|
+
const styleLinks = /((?:url\(|@import\s+(?!url\())\s*)(['"]?)([^'")\s;]+)\2/g
|
|
11
|
+
|
|
12
|
+
const reporter = `{
|
|
13
|
+
let failed = false
|
|
14
|
+
const fail = failure => {
|
|
15
|
+
failed = true
|
|
16
|
+
parent.postMessage({ codecane: 'failed', ...failure }, origin)
|
|
17
|
+
}
|
|
18
|
+
addEventListener('error', event => fail(event instanceof ErrorEvent
|
|
19
|
+
? { message: String(event.error ?? event.message), source: event.filename, line: event.lineno }
|
|
20
|
+
: { message: 'Could not load ' + (event.target.src ?? event.target.href), source: '' }), true)
|
|
21
|
+
addEventListener('unhandledrejection', ({ reason }) => fail({ message: String(reason), source: '' }))
|
|
22
|
+
addEventListener('load', () => failed || parent.postMessage({ codecane: 'ran' }, origin))
|
|
23
|
+
}`
|
|
24
|
+
|
|
25
|
+
export const pathOf = (page, url) => sources[url] ?? (url.startsWith(page.base) ? url.split(/[?#]/)[0].slice(page.base.length) : url)
|
|
26
|
+
|
|
27
|
+
const sheets = (page, drafts, text) => {
|
|
28
|
+
const address = path => new URL(path, page.base).href
|
|
29
|
+
const has = path => page.files.some(file => file.path === path)
|
|
30
|
+
const styles = {}
|
|
31
|
+
const styled = path => styles[path] ??= (async () => {
|
|
32
|
+
const css = drafts[path] ?? await text(path)
|
|
33
|
+
const targets = Object.fromEntries(await Promise.all([...css.matchAll(styleLinks)].map(async ([, , , link]) => {
|
|
34
|
+
const target = new URL(link, address(path)).href
|
|
35
|
+
return [link, pathOf(page, target).endsWith('.css') && has(pathOf(page, target)) ? await styled(pathOf(page, target)) : target]
|
|
36
|
+
})))
|
|
37
|
+
return blob(css.replace(styleLinks, (_, lead, quote, link) => `${lead}${quote}${targets[link]}${quote}`), 'text/css', path)
|
|
38
|
+
})()
|
|
39
|
+
return { address, has, styled }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const restyled = (page, drafts, text) => {
|
|
43
|
+
const { has, styled } = sheets(page, drafts, text)
|
|
44
|
+
return async href => has(pathOf(page, href)) ? styled(pathOf(page, href)) : href
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const mounted = async (page, drafts, text) => {
|
|
48
|
+
const { address, has, styled } = sheets(page, drafts, text)
|
|
49
|
+
|
|
50
|
+
const modules = Object.fromEntries(Object.keys(drafts).filter(path => path.endsWith('.js')).map(path => [
|
|
51
|
+
address(path),
|
|
52
|
+
blob(drafts[path].replace(moduleLinks, (_, lead, quote, link) => `${lead}${quote}${new URL(link, address(path))}${quote}`), 'text/javascript', path),
|
|
53
|
+
]))
|
|
54
|
+
|
|
55
|
+
const doc = new DOMParser().parseFromString(drafts['index.html'] ?? await text('index.html'), 'text/html')
|
|
56
|
+
const report = Object.assign(doc.createElement('script'), { textContent: reporter })
|
|
57
|
+
const base = Object.assign(doc.createElement('base'), { href: page.base })
|
|
58
|
+
const map = doc.querySelector('script[type="importmap"]') ?? Object.assign(doc.createElement('script'), { type: 'importmap', textContent: '{}' })
|
|
59
|
+
const { imports, ...rest } = JSON.parse(map.textContent)
|
|
60
|
+
map.textContent = JSON.stringify({ ...rest, imports: { ...imports, ...modules } })
|
|
61
|
+
doc.head.prepend(report, base, map)
|
|
62
|
+
|
|
63
|
+
await Promise.all([...doc.querySelectorAll('link[rel="stylesheet"][href]')].map(async link => {
|
|
64
|
+
const path = pathOf(page, new URL(link.getAttribute('href'), page.base).href)
|
|
65
|
+
if (has(path)) link.setAttribute('href', await styled(path))
|
|
66
|
+
}))
|
|
67
|
+
doc.querySelectorAll('script[type="module"][src]').forEach(script => {
|
|
68
|
+
const target = new URL(script.getAttribute('src'), page.base).href
|
|
69
|
+
script.setAttribute('src', modules[target] ?? target)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return blob(`<!doctype html>\n${doc.documentElement.outerHTML}`, 'text/html', 'index.html')
|
|
73
|
+
}
|