mirror of https://github.com/sveltejs/svelte
commit
4b9d107eb2
@ -1,5 +0,0 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: hydrate if blocks correctly
|
||||
@ -1,108 +0,0 @@
|
||||
name: ecosystem-ci gate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
if: github.repository == 'sveltejs/svelte' && ((github.event_name == 'pull_request' && github.event.pull_request.head.ref == 'changeset-release/main') || (github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
issues: read
|
||||
contents: read
|
||||
steps:
|
||||
- name: Evaluate gate
|
||||
id: evaluate
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const allowed_roles = new Set(['admin', 'maintain', 'write'])
|
||||
|
||||
const pull_number = context.payload.pull_request
|
||||
? context.payload.pull_request.number
|
||||
: context.issue.number
|
||||
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number,
|
||||
})
|
||||
|
||||
const is_release_pr = pr.head.ref === 'changeset-release/main'
|
||||
if (!is_release_pr) {
|
||||
core.notice(`PR #${pull_number} is not a release PR (${pr.head.ref}). Gate not required.`)
|
||||
core.setOutput('should_fail', 'false')
|
||||
core.setOutput('reason', 'Gate is only required for changeset-release/main')
|
||||
return
|
||||
}
|
||||
|
||||
const { data: commits } = await github.rest.pulls.listCommits({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number,
|
||||
per_page: 250,
|
||||
})
|
||||
|
||||
const last_commit = commits[commits.length - 1]
|
||||
const last_commit_time = new Date(last_commit.commit.committer.date)
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pull_number,
|
||||
per_page: 100,
|
||||
})
|
||||
|
||||
let has_valid_command = false
|
||||
for (const comment of comments) {
|
||||
if (!(comment.body || '').trim().startsWith('/ecosystem-ci run')) {
|
||||
continue
|
||||
}
|
||||
|
||||
const comment_time = new Date(comment.created_at)
|
||||
if (comment_time <= last_commit_time) {
|
||||
continue
|
||||
}
|
||||
|
||||
let permission
|
||||
try {
|
||||
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
username: comment.user.login,
|
||||
})
|
||||
permission = data.permission
|
||||
} catch {
|
||||
permission = 'none'
|
||||
}
|
||||
|
||||
if (allowed_roles.has(permission)) {
|
||||
has_valid_command = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (has_valid_command) {
|
||||
core.setOutput('should_fail', 'false')
|
||||
core.setOutput('reason', 'Valid maintainer /ecosystem-ci run command found after latest commit')
|
||||
return
|
||||
}
|
||||
|
||||
core.setOutput('should_fail', 'true')
|
||||
core.setOutput('reason', 'Release PRs require a maintainer to run /ecosystem-ci after the latest commit')
|
||||
|
||||
- name: Enforce gate
|
||||
if: steps.evaluate.outputs.should_fail == 'true'
|
||||
run: |
|
||||
echo "${{ steps.evaluate.outputs.reason }}"
|
||||
exit 1
|
||||
|
||||
- name: Gate satisfied
|
||||
if: steps.evaluate.outputs.should_fail != 'true'
|
||||
run: echo "${{ steps.evaluate.outputs.reason }}"
|
||||
@ -0,0 +1,82 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
html: `
|
||||
<button>a</button>
|
||||
<button>b</button>
|
||||
<button>resolve a</button>
|
||||
<button>resolve b</button>
|
||||
<p>pending a</p>
|
||||
`,
|
||||
async test({ assert, target }) {
|
||||
const [a, b, resolve_a, resolve_b] = target.querySelectorAll('button');
|
||||
|
||||
resolve_a.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>a</button>
|
||||
<button>b</button>
|
||||
<button>resolve a</button>
|
||||
<button>resolve b</button>
|
||||
<p>page a</p>
|
||||
`
|
||||
);
|
||||
|
||||
b.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>a</button>
|
||||
<button>b</button>
|
||||
<button>resolve a</button>
|
||||
<button>resolve b</button>
|
||||
<p>pending b</p>
|
||||
`
|
||||
);
|
||||
|
||||
a.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>a</button>
|
||||
<button>b</button>
|
||||
<button>resolve a</button>
|
||||
<button>resolve b</button>
|
||||
<p>pending a</p>
|
||||
`
|
||||
);
|
||||
|
||||
resolve_b.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>a</button>
|
||||
<button>b</button>
|
||||
<button>resolve a</button>
|
||||
<button>resolve b</button>
|
||||
<p>pending a</p>
|
||||
`
|
||||
);
|
||||
|
||||
resolve_a.click();
|
||||
await tick();
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<button>a</button>
|
||||
<button>b</button>
|
||||
<button>resolve a</button>
|
||||
<button>resolve b</button>
|
||||
<p>page a</p>
|
||||
`
|
||||
);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,52 @@
|
||||
<script>
|
||||
let page = $state('a');
|
||||
|
||||
/** @type {Array<() => void>} */
|
||||
const a = [];
|
||||
/** @type {Array<() => void>} */
|
||||
const b = [];
|
||||
|
||||
function gate(next) {
|
||||
const deferred = Promise.withResolvers();
|
||||
|
||||
if (next === 'a') a.push(deferred.resolve);
|
||||
else b.push(deferred.resolve);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function nav(next) {
|
||||
page = next;
|
||||
}
|
||||
|
||||
const to_render = $derived(page === 'a' ? snippet_a : snippet_b);
|
||||
</script>
|
||||
|
||||
<button onclick={() => nav('a')}>a</button>
|
||||
<button onclick={() => nav('b')}>b</button>
|
||||
<button onclick={() => a.shift()?.()}>resolve a</button>
|
||||
<button onclick={() => b.shift()?.()}>resolve b</button>
|
||||
|
||||
{#snippet snippet_a()}
|
||||
<svelte:boundary>
|
||||
{@const _a = await gate('a')}
|
||||
<p>page a</p>
|
||||
|
||||
{#snippet pending()}
|
||||
<p>pending a</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
{/snippet}
|
||||
|
||||
{#snippet snippet_b()}
|
||||
<svelte:boundary>
|
||||
{@const _b = await gate('b')}
|
||||
<p>page b</p>
|
||||
|
||||
{#snippet pending()}
|
||||
<p>pending b</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
{/snippet}
|
||||
|
||||
{@render to_render()}
|
||||
@ -0,0 +1,15 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['hydrate', 'async-server', 'client'],
|
||||
ssrHtml: '<p>caught: error (hello)</p>',
|
||||
transformError: () => {
|
||||
return Promise.resolve('error');
|
||||
},
|
||||
|
||||
async test({ assert, target }) {
|
||||
await tick();
|
||||
assert.htmlEqual(target.innerHTML, '<p>caught: error (hello)</p>');
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,14 @@
|
||||
<script>
|
||||
import { get } from "./main.svelte";
|
||||
|
||||
let { error } = $props();
|
||||
const context = get()
|
||||
</script>
|
||||
|
||||
{#if error}
|
||||
<p>caught: {await error} ({context})</p>
|
||||
{:else}
|
||||
{(() => {
|
||||
throw 'catch me';
|
||||
})()}
|
||||
{/if}
|
||||
@ -0,0 +1,19 @@
|
||||
<script module>
|
||||
import { createContext } from "svelte";
|
||||
import Child from "./child.svelte";
|
||||
|
||||
const [ get, set ] = createContext();
|
||||
export {get};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
set('hello');
|
||||
</script>
|
||||
|
||||
<svelte:boundary>
|
||||
{#snippet failed(error)}
|
||||
<Child {error} />
|
||||
{/snippet}
|
||||
|
||||
<Child />
|
||||
</svelte:boundary>
|
||||
@ -0,0 +1,7 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, logs }) {
|
||||
assert.deepEqual(logs, [42, 43]);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,12 @@
|
||||
<script>
|
||||
let value = $state(42);
|
||||
|
||||
function shadow(output = value) {
|
||||
const value = 1337;
|
||||
return output;
|
||||
}
|
||||
|
||||
console.log(shadow());
|
||||
value += 1;
|
||||
console.log(shadow());
|
||||
</script>
|
||||
@ -0,0 +1,8 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
props: {
|
||||
query: '--!><img src=x onerror=alert(1)><!--'
|
||||
},
|
||||
transformError: (error) => ({ message: /** @type {Error} */ (error).message })
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
<p class="error">--!><img src=x onerror=alert(1)><!--</p>
|
||||
@ -0,0 +1,15 @@
|
||||
<script>
|
||||
let { query } = $props();
|
||||
|
||||
function search(q) {
|
||||
throw new Error(q);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:boundary>
|
||||
<p>{search(query)}</p>
|
||||
|
||||
{#snippet failed(error)}
|
||||
<p class="error">{error.message}</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
@ -0,0 +1,8 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
props: {
|
||||
query: '--><img src=x onerror=alert(1)><!--'
|
||||
},
|
||||
transformError: (error) => ({ message: /** @type {Error} */ (error).message })
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
<p class="error">--><img src=x onerror=alert(1)><!--</p>
|
||||
@ -0,0 +1,15 @@
|
||||
<script>
|
||||
let { query } = $props();
|
||||
|
||||
function search(q) {
|
||||
throw new Error(q);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:boundary>
|
||||
<p>{search(query)}</p>
|
||||
|
||||
{#snippet failed(error)}
|
||||
<p class="error">{error.message}</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
@ -0,0 +1,8 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
props: {
|
||||
query: '<!--<script>alert(1)</script>'
|
||||
},
|
||||
transformError: (error) => ({ message: /** @type {Error} */ (error).message })
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
<p class="error"><!--<script>alert(1)</script></p>
|
||||
@ -0,0 +1,15 @@
|
||||
<script>
|
||||
let { query } = $props();
|
||||
|
||||
function search(q) {
|
||||
throw new Error(q);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:boundary>
|
||||
<p>{search(query)}</p>
|
||||
|
||||
{#snippet failed(error)}
|
||||
<p class="error">{error.message}</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
@ -0,0 +1,8 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
props: {
|
||||
query: '<!--><!--->-->'
|
||||
},
|
||||
transformError: (error) => ({ message: /** @type {Error} */ (error).message })
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
<p class="error"><!--><!--->--></p>
|
||||
@ -0,0 +1,15 @@
|
||||
<script>
|
||||
let { query } = $props();
|
||||
|
||||
function search(q) {
|
||||
throw new Error(q);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:boundary>
|
||||
<p>{search(query)}</p>
|
||||
|
||||
{#snippet failed(error)}
|
||||
<p class="error">{error.message}</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
@ -0,0 +1 @@
|
||||
<!--[--><div contenteditable=""><script>alert('pwnd')</script></div> <div contenteditable=""><script>alert('pwnd')</script></div> <div contenteditable=""><script>alert('pwnd')</script></div><!--]-->
|
||||
@ -0,0 +1,7 @@
|
||||
<script>
|
||||
let data = $state("<scri"+"pt>alert('pwnd')</scr"+"ipt>");
|
||||
</script>
|
||||
|
||||
<div contenteditable bind:innerText={data}></div>
|
||||
<div contenteditable bind:textContent={data}></div>
|
||||
<div contenteditable bind:innerHTML={data}></div>
|
||||
Loading…
Reference in new issue