mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-11 13:48:40 +00:00
Merge pull request #664 from tidalcycles/superdough
superdough: encapsulates web audio output
This commit is contained in:
commit
834bb7d777
165
packages/superdough/README.md
Normal file
165
packages/superdough/README.md
Normal file
@ -0,0 +1,165 @@
|
||||
# superdough
|
||||
|
||||
superdough is a simple web audio sampler and synth, intended for live coding.
|
||||
It is the default output of [strudel](https://strudel.tidalcycles.org/).
|
||||
This package has no ties to strudel and can be used to quickly bake your own music system on the web.
|
||||
|
||||
## Install
|
||||
|
||||
via npm:
|
||||
|
||||
```js
|
||||
npm i superdough --save
|
||||
```
|
||||
|
||||
## Use
|
||||
|
||||
```js
|
||||
import { superdough, samples, initAudioOnFirstClick, registerSynthSounds } from 'superdough';
|
||||
|
||||
const init = Promise.all([
|
||||
initAudioOnFirstClick(),
|
||||
samples('github:tidalcycles/Dirt-Samples/master'),
|
||||
registerSynthSounds(),
|
||||
]);
|
||||
|
||||
const loop = (t = 0) => {
|
||||
// superdough(value, time, duration)
|
||||
superdough({ s: 'bd', delay: 0.5 }, t);
|
||||
superdough({ note: 'g1', s: 'sawtooth', cutoff: 600, resonance: 8 }, t, 0.125);
|
||||
superdough({ note: 'g2', s: 'sawtooth', cutoff: 600, resonance: 8 }, t + 0.25, 0.125);
|
||||
superdough({ s: 'hh' }, t + 0.25);
|
||||
superdough({ s: 'sd', room: 0.5 }, t + 0.5);
|
||||
superdough({ s: 'hh' }, t + 0.75);
|
||||
};
|
||||
|
||||
document.getElementById('play').addEventListener('click', async () => {
|
||||
await init;
|
||||
let t = 0.1;
|
||||
while (t < 16) {
|
||||
loop(t++);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
[Open this in Codesandbox](https://codesandbox.io/s/superdough-demo-forked-sf8djh?file=/src/index.js)
|
||||
|
||||
## API
|
||||
|
||||
### superdough(value, deadline, duration)
|
||||
|
||||
```js
|
||||
superdough({ s: 'bd', delay: 0.5 }, 0, 1);
|
||||
```
|
||||
|
||||
- `value`: the sound properties:
|
||||
- `s`: the name of the sound as loaded via `samples` or `registerSound`
|
||||
- `n`: selects sample with given index
|
||||
- `bank`: prefix_ that is attached to the sound, e.g. `{ s: 'bd', bank: 'RolandTR909' }` = `{ s: 'RolandTR909_bd' }`
|
||||
- `gain`: gain from 0 to 1 (higher values also work but might clip)
|
||||
- `velocity`: additional gain multiplier
|
||||
- `cutoff`: low pass filter cutoff
|
||||
- `resonance`: low pass filter resonance
|
||||
- `hcutoff`: high pass filter cutoff
|
||||
- `hresonance`: high pass filter resonance
|
||||
- `bandf`: band pass filter cutoff
|
||||
- `bandq`: band pass filter resonance
|
||||
- `crush`: amplitude bit crusher using given number of bits
|
||||
- `shape`: distortion effect from 0 (none) to 1 (full). might get loud!
|
||||
- `pan`: stereo panning from 0 (left) to 1 (right)
|
||||
- `vowel`: vowel filter. possible values: "a", "e", "i", "o", "u"
|
||||
- `delay`: delay mix
|
||||
- `delayfeedback`: delay feedback
|
||||
- `delaytime`: delay time
|
||||
- `room`: reverb mix
|
||||
- `size`: reverb room size
|
||||
- `orbit`: bus name for global effects `delay` and `room`. same orbits will get the same effects
|
||||
- `freq`: repitches sound to given frequency in Hz
|
||||
- `note`: repitches sound to given note or midi number
|
||||
- `cut`: sets cut group. Sounds of same group will cut each other off
|
||||
- `clip`: multiplies duration with given number
|
||||
- `speed`: repitches sound by given factor
|
||||
- `begin`: moves beginning of sample to given factor (between 0 and 1)
|
||||
- `end`: moves end of sample to given factor (between 0 and 1)
|
||||
- `attack`: seconds of attack phase
|
||||
- `decay`: seconds of decay phase
|
||||
- `sustain`: gain of sustain phase
|
||||
- `release`: seconds of release phase
|
||||
- `deadline`: seconds until the sound should play (0 = immediate)
|
||||
- `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds
|
||||
|
||||
### registerSynthSounds()
|
||||
|
||||
Loads the default waveforms `sawtooth`, `square`, `triangle` and `sine`. Use them like this:
|
||||
|
||||
```js
|
||||
superdough({ s:'sawtooth' }, 0, 1)
|
||||
```
|
||||
|
||||
The duration needs to be set for these sounds!
|
||||
|
||||
### samples(sampleMap)
|
||||
|
||||
allows you to load samples from URLs. There are 3 ways to load samples
|
||||
|
||||
1. sample map object
|
||||
2. url of sample map json file
|
||||
3. github repo
|
||||
|
||||
#### sample map object
|
||||
|
||||
You can pass a sample map like this:
|
||||
|
||||
```js
|
||||
samples({
|
||||
'_base': 'https://raw.githubusercontent.com/felixroos/samples/main/',
|
||||
'bd': 'president/president_bd.mp3',
|
||||
'sd': ['president/president_sd.mp3', 'president/president_sd2.mp3'],
|
||||
'hh': ['president/president_hh.mp3'],
|
||||
})
|
||||
```
|
||||
|
||||
The `_base` property defines the root url while the others declare one or more sample paths for each sound.
|
||||
|
||||
For example the full URL for `bd` would then be `https://raw.githubusercontent.com/felixroos/samples/main/president/president_bd.mp3`
|
||||
|
||||
A loaded sound can then be played with `superdough({ s: 'bd' }, 0)`.
|
||||
|
||||
If you declare multiple sounds, you can select them with `n`: `superdough({ s: 'sd', n: 1 }, 0)`
|
||||
|
||||
The duration property is not needed for samples.
|
||||
|
||||
#### loading samples from a json file
|
||||
|
||||
Instead of passing an object as a sample map, you can also pass a URL to a json that contains a sample map:
|
||||
|
||||
```js
|
||||
samples('https://raw.githubusercontent.com/felixroos/samples/main/strudel.json')
|
||||
```
|
||||
|
||||
The json file is expected to have the same format as described above.
|
||||
|
||||
#### loading samples from a github repo
|
||||
|
||||
Because it is common to use github for samples, there is a short way to load a sample map from github:
|
||||
|
||||
```js
|
||||
samples('github:tidalcycles/Dirt-Samples/master')
|
||||
```
|
||||
|
||||
The format is `github:<user>/<repo>/<branch>`.
|
||||
|
||||
It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo.
|
||||
|
||||
The format is also expected to be the same as explained above.
|
||||
|
||||
### initAudioOnFirstClick()
|
||||
|
||||
Initializes audio and makes sure it is playable after the first click in the document. A click is needed because of the [Autoplay Policy](https://www.w3.org/TR/autoplay-detection/).
|
||||
You can call this function when the document loads.
|
||||
Then just make sure your first call of `superdough` happens after a click of something.
|
||||
|
||||
## Credits
|
||||
|
||||
- [SuperDirt](https://github.com/musikinformatik/SuperDirt)
|
||||
- [WebDirt](https://github.com/dktr0/WebDirt)
|
||||
24
packages/superdough/example/.gitignore
vendored
Normal file
24
packages/superdough/example/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
12
packages/superdough/example/index.html
Normal file
12
packages/superdough/example/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Superdough Example</title>
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<button id="play">PLAAAAAAAY</button>
|
||||
<script src="/main.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
25
packages/superdough/example/main.js
Normal file
25
packages/superdough/example/main.js
Normal file
@ -0,0 +1,25 @@
|
||||
import { superdough, samples, initAudioOnFirstClick, registerSynthSounds } from 'superdough';
|
||||
|
||||
const init = Promise.all([
|
||||
initAudioOnFirstClick(),
|
||||
samples('github:tidalcycles/Dirt-Samples/master'),
|
||||
registerSynthSounds(),
|
||||
]);
|
||||
|
||||
const loop = (t = 0) => {
|
||||
// superdough(value, time, duration)
|
||||
superdough({ s: 'bd', delay: 0.5 }, t);
|
||||
superdough({ note: 'g1', s: 'sawtooth', cutoff: 600, resonance: 8 }, t, 0.125);
|
||||
superdough({ note: 'g2', s: 'sawtooth', cutoff: 600, resonance: 8 }, t + 0.25, 0.125);
|
||||
superdough({ s: 'hh' }, t + 0.25);
|
||||
superdough({ s: 'sd', room: 0.5 }, t + 0.5);
|
||||
superdough({ s: 'hh' }, t + 0.75);
|
||||
};
|
||||
|
||||
document.getElementById('play').addEventListener('click', async () => {
|
||||
await init;
|
||||
let t = 0.1;
|
||||
while (t < 16) {
|
||||
loop(t++);
|
||||
}
|
||||
});
|
||||
17
packages/superdough/example/package.json
Normal file
17
packages/superdough/example/package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "superdough-example",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"superdough": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.4.5"
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { getAudioContext } from './webaudio.mjs';
|
||||
import { getAudioContext } from './superdough.mjs';
|
||||
|
||||
export function gainNode(value) {
|
||||
const node = getAudioContext().createGain();
|
||||
11
packages/superdough/index.mjs
Normal file
11
packages/superdough/index.mjs
Normal file
@ -0,0 +1,11 @@
|
||||
/*
|
||||
index.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/superdough/index.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export * from './superdough.mjs';
|
||||
export * from './sampler.mjs';
|
||||
export * from './helpers.mjs';
|
||||
export * from './synth.mjs';
|
||||
export * from './logger.mjs';
|
||||
7
packages/superdough/logger.mjs
Normal file
7
packages/superdough/logger.mjs
Normal file
@ -0,0 +1,7 @@
|
||||
let log = (msg) => console.log(msg);
|
||||
|
||||
export const logger = (...args) => log(...args);
|
||||
|
||||
export const setLogger = (fn) => {
|
||||
log = fn;
|
||||
};
|
||||
41
packages/superdough/package.json
Normal file
41
packages/superdough/package.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "superdough",
|
||||
"version": "0.9.3",
|
||||
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.mjs"
|
||||
},
|
||||
"directories": {
|
||||
"example": "examples"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"nanostores": "^0.8.1"
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { logger, noteToMidi, valueToMidi } from '@strudel.cycles/core';
|
||||
import { noteToMidi, valueToMidi } from './util.mjs';
|
||||
import { getAudioContext, registerSound } from './index.mjs';
|
||||
import { getEnvelope } from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||
252
packages/superdough/superdough.mjs
Normal file
252
packages/superdough/superdough.mjs
Normal file
@ -0,0 +1,252 @@
|
||||
/*
|
||||
superdough.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/superdough/superdough.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import './feedbackdelay.mjs';
|
||||
import './reverb.mjs';
|
||||
import './vowel.mjs';
|
||||
import { clamp } from './util.mjs';
|
||||
import workletsUrl from './worklets.mjs?url';
|
||||
import { getFilter, gainNode } from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
export const soundMap = map();
|
||||
export function registerSound(key, onTrigger, data = {}) {
|
||||
soundMap.setKey(key, { onTrigger, data });
|
||||
}
|
||||
export function getSound(s) {
|
||||
return soundMap.get()[s];
|
||||
}
|
||||
export const resetLoadedSounds = () => soundMap.set({});
|
||||
|
||||
let audioContext;
|
||||
export const getAudioContext = () => {
|
||||
if (!audioContext) {
|
||||
audioContext = new AudioContext();
|
||||
}
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
let destination;
|
||||
const getDestination = () => {
|
||||
const ctx = getAudioContext();
|
||||
if (!destination) {
|
||||
destination = ctx.createGain();
|
||||
destination.connect(ctx.destination);
|
||||
}
|
||||
return destination;
|
||||
};
|
||||
|
||||
export const panic = () => {
|
||||
getDestination().gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
|
||||
destination = null;
|
||||
};
|
||||
|
||||
let workletsLoading;
|
||||
function loadWorklets() {
|
||||
if (workletsLoading) {
|
||||
return workletsLoading;
|
||||
}
|
||||
workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl);
|
||||
return workletsLoading;
|
||||
}
|
||||
|
||||
function getWorklet(ac, processor, params) {
|
||||
const node = new AudioWorkletNode(ac, processor);
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
node.parameters.get(key).value = value;
|
||||
});
|
||||
return node;
|
||||
}
|
||||
|
||||
// this function should be called on first user interaction (to avoid console warning)
|
||||
export async function initAudio(options = {}) {
|
||||
const { disableWorklets = false } = options;
|
||||
if (typeof window !== 'undefined') {
|
||||
await getAudioContext().resume();
|
||||
if (!disableWorklets) {
|
||||
await loadWorklets().catch((err) => {
|
||||
console.warn('could not load AudioWorklet effects coarse, crush and shape', err);
|
||||
});
|
||||
} else {
|
||||
console.log('disableWorklets: AudioWorklet effects coarse, crush and shape are skipped!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function initAudioOnFirstClick(options) {
|
||||
return new Promise((resolve) => {
|
||||
document.addEventListener('click', async function listener() {
|
||||
await initAudio(options);
|
||||
resolve();
|
||||
document.removeEventListener('click', listener);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let delays = {};
|
||||
const maxfeedback = 0.98;
|
||||
function getDelay(orbit, delaytime, delayfeedback, t) {
|
||||
if (delayfeedback > maxfeedback) {
|
||||
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
|
||||
}
|
||||
delayfeedback = clamp(delayfeedback, 0, 0.98);
|
||||
if (!delays[orbit]) {
|
||||
const ac = getAudioContext();
|
||||
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
||||
dly.start?.(t); // for some reason, this throws when audion extension is installed..
|
||||
dly.connect(getDestination());
|
||||
delays[orbit] = dly;
|
||||
}
|
||||
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
|
||||
delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t);
|
||||
return delays[orbit];
|
||||
}
|
||||
|
||||
let reverbs = {};
|
||||
function getReverb(orbit, duration = 2) {
|
||||
if (!reverbs[orbit]) {
|
||||
const ac = getAudioContext();
|
||||
const reverb = ac.createReverb(duration);
|
||||
reverb.connect(getDestination());
|
||||
reverbs[orbit] = reverb;
|
||||
}
|
||||
if (reverbs[orbit].duration !== duration) {
|
||||
reverbs[orbit] = reverbs[orbit].setDuration(duration);
|
||||
reverbs[orbit].duration = duration;
|
||||
}
|
||||
return reverbs[orbit];
|
||||
}
|
||||
|
||||
function effectSend(input, effect, wet) {
|
||||
const send = gainNode(wet);
|
||||
input.connect(send);
|
||||
send.connect(effect);
|
||||
return send;
|
||||
}
|
||||
|
||||
export const superdough = async (value, deadline, hapDuration) => {
|
||||
const ac = getAudioContext();
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error(
|
||||
`expected hap.value to be an object, but got "${value}". Hint: append .note() or .s() to the end`,
|
||||
'error',
|
||||
);
|
||||
}
|
||||
|
||||
// calculate absolute time
|
||||
let t = ac.currentTime + deadline;
|
||||
// destructure
|
||||
let {
|
||||
s = 'triangle',
|
||||
bank,
|
||||
source,
|
||||
gain = 0.8,
|
||||
// low pass
|
||||
cutoff,
|
||||
resonance = 1,
|
||||
// high pass
|
||||
hcutoff,
|
||||
hresonance = 1,
|
||||
// band pass
|
||||
bandf,
|
||||
bandq = 1,
|
||||
//
|
||||
coarse,
|
||||
crush,
|
||||
shape,
|
||||
pan,
|
||||
vowel,
|
||||
delay = 0,
|
||||
delayfeedback = 0.5,
|
||||
delaytime = 0.25,
|
||||
orbit = 1,
|
||||
room,
|
||||
size = 2,
|
||||
velocity = 1,
|
||||
} = value;
|
||||
gain *= velocity; // legacy fix for velocity
|
||||
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
|
||||
const onended = () => {
|
||||
toDisconnect.forEach((n) => n?.disconnect());
|
||||
};
|
||||
if (bank && s) {
|
||||
s = `${bank}_${s}`;
|
||||
}
|
||||
// get source AudioNode
|
||||
let sourceNode;
|
||||
if (source) {
|
||||
sourceNode = source(t, value, hapDuration);
|
||||
} else if (getSound(s)) {
|
||||
const { onTrigger } = getSound(s);
|
||||
const soundHandle = await onTrigger(t, value, onended);
|
||||
if (soundHandle) {
|
||||
sourceNode = soundHandle.node;
|
||||
soundHandle.stop(t + hapDuration);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`sound ${s} not found! Is it loaded?`);
|
||||
}
|
||||
if (!sourceNode) {
|
||||
// if onTrigger does not return anything, we will just silently skip
|
||||
// this can be used for things like speed(0) in the sampler
|
||||
return;
|
||||
}
|
||||
if (ac.currentTime > t) {
|
||||
logger('[webaudio] skip hap: still loading', ac.currentTime - t);
|
||||
return;
|
||||
}
|
||||
const chain = []; // audio nodes that will be connected to each other sequentially
|
||||
chain.push(sourceNode);
|
||||
|
||||
// gain stage
|
||||
chain.push(gainNode(gain));
|
||||
|
||||
// filters
|
||||
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
|
||||
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
|
||||
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq));
|
||||
vowel !== undefined && chain.push(ac.createVowelFilter(vowel));
|
||||
|
||||
// effects
|
||||
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
||||
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape }));
|
||||
|
||||
// panning
|
||||
if (pan !== undefined) {
|
||||
const panner = ac.createStereoPanner();
|
||||
panner.pan.value = 2 * pan - 1;
|
||||
chain.push(panner);
|
||||
}
|
||||
|
||||
// last gain
|
||||
const post = gainNode(1);
|
||||
chain.push(post);
|
||||
post.connect(getDestination());
|
||||
|
||||
// delay
|
||||
let delaySend;
|
||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||
const delyNode = getDelay(orbit, delaytime, delayfeedback, t);
|
||||
delaySend = effectSend(post, delyNode, delay);
|
||||
}
|
||||
// reverb
|
||||
let reverbSend;
|
||||
if (room > 0 && size > 0) {
|
||||
const reverbNode = getReverb(orbit, size);
|
||||
reverbSend = effectSend(post, reverbNode, room);
|
||||
}
|
||||
|
||||
// connect chain elements together
|
||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||
|
||||
// toDisconnect = all the node that should be disconnected in onended callback
|
||||
// this is crucial for performance
|
||||
toDisconnect = chain.concat([delaySend, reverbSend]);
|
||||
};
|
||||
|
||||
export const superdoughTrigger = (t, hap, ct, cps) => superdough(hap, t - ct, hap.duration / cps, cps);
|
||||
@ -1,5 +1,5 @@
|
||||
import { midiToFreq, noteToMidi } from '@strudel.cycles/core';
|
||||
import { registerSound } from './webaudio.mjs';
|
||||
import { midiToFreq, noteToMidi } from './util.mjs';
|
||||
import { registerSound } from './superdough.mjs';
|
||||
import { getOscillator, gainNode, getEnvelope } from './helpers.mjs';
|
||||
|
||||
export function registerSynthSounds() {
|
||||
53
packages/superdough/util.mjs
Normal file
53
packages/superdough/util.mjs
Normal file
@ -0,0 +1,53 @@
|
||||
// currently duplicate with core util.mjs to skip dependency
|
||||
// TODO: add separate util module?
|
||||
|
||||
export const tokenizeNote = (note) => {
|
||||
if (typeof note !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
|
||||
if (!pc) {
|
||||
return [];
|
||||
}
|
||||
return [pc, acc, oct ? Number(oct) : undefined];
|
||||
};
|
||||
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
||||
const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
||||
|
||||
export const noteToMidi = (note, defaultOctave = 3) => {
|
||||
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
||||
if (!pc) {
|
||||
throw new Error('not a note: "' + note + '"');
|
||||
}
|
||||
const chroma = chromas[pc.toLowerCase()];
|
||||
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||
};
|
||||
export const midiToFreq = (n) => {
|
||||
return Math.pow(2, (n - 69) / 12) * 440;
|
||||
};
|
||||
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||
|
||||
export const freqToMidi = (freq) => {
|
||||
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
|
||||
};
|
||||
|
||||
export const valueToMidi = (value, fallbackValue) => {
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error('valueToMidi: expected object value');
|
||||
}
|
||||
let { freq, note } = value;
|
||||
if (typeof freq === 'number') {
|
||||
return freqToMidi(freq);
|
||||
}
|
||||
if (typeof note === 'string') {
|
||||
return noteToMidi(note);
|
||||
}
|
||||
if (typeof note === 'number') {
|
||||
return note;
|
||||
}
|
||||
if (!fallbackValue) {
|
||||
throw new Error('valueToMidi: expected freq or note to be set');
|
||||
}
|
||||
return fallbackValue;
|
||||
};
|
||||
19
packages/superdough/vite.config.js
Normal file
19
packages/superdough/vite.config.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { dependencies } from './package.json';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'index.mjs'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.cjs' }[ext]),
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
@ -1,6 +1,7 @@
|
||||
# @strudel.cycles/webaudio
|
||||
|
||||
This package contains helpers to make music with strudel and the Web Audio API.
|
||||
It is a thin binding to [superdough](https://www.npmjs.com/package/superdough).
|
||||
|
||||
## Install
|
||||
|
||||
|
||||
@ -5,6 +5,4 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
export * from './webaudio.mjs';
|
||||
export * from './sampler.mjs';
|
||||
export * from './helpers.mjs';
|
||||
export * from './synth.mjs';
|
||||
export * from 'superdough';
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"nanostores": "^0.8.1"
|
||||
"superdough": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3"
|
||||
|
||||
@ -5,247 +5,21 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
import './feedbackdelay.mjs';
|
||||
import './reverb.mjs';
|
||||
import { superdough, getAudioContext, setLogger } from 'superdough';
|
||||
const { Pattern, logger } = strudel;
|
||||
import './vowel.mjs';
|
||||
import workletsUrl from './worklets.mjs?url';
|
||||
import { getFilter, gainNode } from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
|
||||
export const soundMap = map();
|
||||
export function registerSound(key, onTrigger, data = {}) {
|
||||
soundMap.setKey(key, { onTrigger, data });
|
||||
}
|
||||
export function getSound(s) {
|
||||
return soundMap.get()[s];
|
||||
}
|
||||
export const resetLoadedSounds = () => soundMap.set({});
|
||||
setLogger(logger);
|
||||
|
||||
let audioContext;
|
||||
export const getAudioContext = () => {
|
||||
if (!audioContext) {
|
||||
audioContext = new AudioContext();
|
||||
}
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
let destination;
|
||||
const getDestination = () => {
|
||||
const ctx = getAudioContext();
|
||||
if (!destination) {
|
||||
destination = ctx.createGain();
|
||||
destination.connect(ctx.destination);
|
||||
}
|
||||
return destination;
|
||||
};
|
||||
|
||||
export const panic = () => {
|
||||
getDestination().gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
|
||||
destination = null;
|
||||
};
|
||||
|
||||
let workletsLoading;
|
||||
function loadWorklets() {
|
||||
if (workletsLoading) {
|
||||
return workletsLoading;
|
||||
}
|
||||
workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl);
|
||||
return workletsLoading;
|
||||
}
|
||||
|
||||
function getWorklet(ac, processor, params) {
|
||||
const node = new AudioWorkletNode(ac, processor);
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
node.parameters.get(key).value = value;
|
||||
});
|
||||
return node;
|
||||
}
|
||||
|
||||
// this function should be called on first user interaction (to avoid console warning)
|
||||
export async function initAudio() {
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
await getAudioContext().resume();
|
||||
await loadWorklets();
|
||||
} catch (err) {
|
||||
console.warn('could not load AudioWorklet effects coarse, crush and shape', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function initAudioOnFirstClick() {
|
||||
return new Promise((resolve) => {
|
||||
document.addEventListener('click', async function listener() {
|
||||
await initAudio();
|
||||
resolve();
|
||||
document.removeEventListener('click', listener);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let delays = {};
|
||||
const maxfeedback = 0.98;
|
||||
function getDelay(orbit, delaytime, delayfeedback, t) {
|
||||
if (delayfeedback > maxfeedback) {
|
||||
logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
|
||||
}
|
||||
delayfeedback = strudel.clamp(delayfeedback, 0, 0.98);
|
||||
if (!delays[orbit]) {
|
||||
const ac = getAudioContext();
|
||||
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
||||
dly.start?.(t); // for some reason, this throws when audion extension is installed..
|
||||
dly.connect(getDestination());
|
||||
delays[orbit] = dly;
|
||||
}
|
||||
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
|
||||
delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t);
|
||||
return delays[orbit];
|
||||
}
|
||||
|
||||
let reverbs = {};
|
||||
function getReverb(orbit, duration = 2) {
|
||||
if (!reverbs[orbit]) {
|
||||
const ac = getAudioContext();
|
||||
const reverb = ac.createReverb(duration);
|
||||
reverb.connect(getDestination());
|
||||
reverbs[orbit] = reverb;
|
||||
}
|
||||
if (reverbs[orbit].duration !== duration) {
|
||||
reverbs[orbit] = reverbs[orbit].setDuration(duration);
|
||||
reverbs[orbit].duration = duration;
|
||||
}
|
||||
return reverbs[orbit];
|
||||
}
|
||||
|
||||
function effectSend(input, effect, wet) {
|
||||
const send = gainNode(wet);
|
||||
input.connect(send);
|
||||
send.connect(effect);
|
||||
return send;
|
||||
}
|
||||
|
||||
// export const webaudioOutput = async (t, hap, ct, cps) => {
|
||||
export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
|
||||
const ac = getAudioContext();
|
||||
const hap2value = (hap) => {
|
||||
hap.ensureObjectValue();
|
||||
|
||||
// calculate absolute time
|
||||
let t = ac.currentTime + deadline;
|
||||
// destructure
|
||||
let {
|
||||
s = 'triangle',
|
||||
bank,
|
||||
source,
|
||||
gain = 0.8,
|
||||
// low pass
|
||||
cutoff,
|
||||
resonance = 1,
|
||||
// high pass
|
||||
hcutoff,
|
||||
hresonance = 1,
|
||||
// band pass
|
||||
bandf,
|
||||
bandq = 1,
|
||||
//
|
||||
coarse,
|
||||
crush,
|
||||
shape,
|
||||
pan,
|
||||
vowel,
|
||||
delay = 0,
|
||||
delayfeedback = 0.5,
|
||||
delaytime = 0.25,
|
||||
orbit = 1,
|
||||
room,
|
||||
size = 2,
|
||||
} = hap.value;
|
||||
const { velocity = 1 } = hap.context;
|
||||
gain *= velocity; // legacy fix for velocity
|
||||
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
|
||||
const onended = () => {
|
||||
toDisconnect.forEach((n) => n?.disconnect());
|
||||
};
|
||||
if (bank && s) {
|
||||
s = `${bank}_${s}`;
|
||||
}
|
||||
// get source AudioNode
|
||||
let sourceNode;
|
||||
if (source) {
|
||||
sourceNode = source(t, hap.value, hapDuration);
|
||||
} else if (getSound(s)) {
|
||||
const { onTrigger } = getSound(s);
|
||||
const soundHandle = await onTrigger(t, hap.value, onended);
|
||||
if (soundHandle) {
|
||||
sourceNode = soundHandle.node;
|
||||
soundHandle.stop(t + hapDuration);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`sound ${s} not found! Is it loaded?`);
|
||||
}
|
||||
if (!sourceNode) {
|
||||
// if onTrigger does not return anything, we will just silently skip
|
||||
// this can be used for things like speed(0) in the sampler
|
||||
return;
|
||||
}
|
||||
if (ac.currentTime > t) {
|
||||
logger('[webaudio] skip hap: still loading', ac.currentTime - t);
|
||||
return;
|
||||
}
|
||||
const chain = []; // audio nodes that will be connected to each other sequentially
|
||||
chain.push(sourceNode);
|
||||
|
||||
// gain stage
|
||||
chain.push(gainNode(gain));
|
||||
|
||||
// filters
|
||||
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
|
||||
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
|
||||
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq));
|
||||
vowel !== undefined && chain.push(ac.createVowelFilter(vowel));
|
||||
|
||||
// effects
|
||||
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
||||
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape }));
|
||||
|
||||
// panning
|
||||
if (pan !== undefined) {
|
||||
const panner = ac.createStereoPanner();
|
||||
panner.pan.value = 2 * pan - 1;
|
||||
chain.push(panner);
|
||||
}
|
||||
|
||||
// last gain
|
||||
const post = gainNode(1);
|
||||
chain.push(post);
|
||||
post.connect(getDestination());
|
||||
|
||||
// delay
|
||||
let delaySend;
|
||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||
const delyNode = getDelay(orbit, delaytime, delayfeedback, t);
|
||||
delaySend = effectSend(post, delyNode, delay);
|
||||
}
|
||||
// reverb
|
||||
let reverbSend;
|
||||
if (room > 0 && size > 0) {
|
||||
const reverbNode = getReverb(orbit, size);
|
||||
reverbSend = effectSend(post, reverbNode, room);
|
||||
}
|
||||
|
||||
// connect chain elements together
|
||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||
|
||||
// toDisconnect = all the node that should be disconnected in onended callback
|
||||
// this is crucial for performance
|
||||
toDisconnect = chain.concat([delaySend, reverbSend]);
|
||||
return { ...hap.value, velocity: hap.context.velocity };
|
||||
};
|
||||
|
||||
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps, cps);
|
||||
// TODO: bind logger
|
||||
export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps);
|
||||
export const webaudioOutput = (hap, deadline, hapDuration) => superdough(hap2value(hap), deadline, hapDuration);
|
||||
|
||||
Pattern.prototype.webaudio = function () {
|
||||
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
|
||||
return this.onTrigger(webaudioOutputTrigger);
|
||||
};
|
||||
|
||||
|
||||
343
pnpm-lock.yaml
generated
343
pnpm-lock.yaml
generated
@ -386,6 +386,26 @@ importers:
|
||||
specifier: ^4.3.3
|
||||
version: 4.3.3(@types/node@18.16.3)
|
||||
|
||||
packages/superdough:
|
||||
dependencies:
|
||||
nanostores:
|
||||
specifier: ^0.8.1
|
||||
version: 0.8.1
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^4.3.3
|
||||
version: 4.3.3(@types/node@18.16.3)
|
||||
|
||||
packages/superdough/example:
|
||||
dependencies:
|
||||
superdough:
|
||||
specifier: workspace:*
|
||||
version: link:..
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^4.4.5
|
||||
version: 4.4.5(@types/node@18.16.3)
|
||||
|
||||
packages/tonal:
|
||||
dependencies:
|
||||
'@strudel.cycles/core':
|
||||
@ -470,9 +490,9 @@ importers:
|
||||
'@strudel.cycles/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
nanostores:
|
||||
specifier: ^0.8.1
|
||||
version: 0.8.1
|
||||
superdough:
|
||||
specifier: workspace:*
|
||||
version: link:../superdough
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^4.3.3
|
||||
@ -498,7 +518,7 @@ importers:
|
||||
version: 4.17.0
|
||||
'@astrojs/mdx':
|
||||
specifier: ^0.19.0
|
||||
version: 0.19.0(astro@2.3.2)(rollup@3.21.0)
|
||||
version: 0.19.0(astro@2.3.2)(rollup@3.28.0)
|
||||
'@astrojs/react':
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1(@types/react-dom@18.2.1)(@types/react@18.2.0)(react-dom@18.2.0)(react@18.2.0)
|
||||
@ -628,7 +648,7 @@ importers:
|
||||
version: 3.0.3
|
||||
vite-plugin-pwa:
|
||||
specifier: ^0.14.7
|
||||
version: 0.14.7(vite@4.3.3)(workbox-build@6.5.4)(workbox-window@6.5.4)
|
||||
version: 0.14.7(vite@4.4.5)(workbox-build@6.5.4)(workbox-window@6.5.4)
|
||||
workbox-window:
|
||||
specifier: ^6.5.4
|
||||
version: 6.5.4
|
||||
@ -849,14 +869,14 @@ packages:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/@astrojs/mdx@0.19.0(astro@2.3.2)(rollup@3.21.0):
|
||||
/@astrojs/mdx@0.19.0(astro@2.3.2)(rollup@3.28.0):
|
||||
resolution: {integrity: sha512-McFpMV+npinIEKnY5t9hsdzLd76g78GgIRUPxem2OeXPNB8xr2pNS28GeU0+6Pn5STnB+sgcyyeqXLgzauOlMQ==}
|
||||
engines: {node: '>=16.12.0'}
|
||||
dependencies:
|
||||
'@astrojs/markdown-remark': 2.1.4(astro@2.3.2)
|
||||
'@astrojs/prism': 2.1.1
|
||||
'@mdx-js/mdx': 2.3.0
|
||||
'@mdx-js/rollup': 2.3.0(rollup@3.21.0)
|
||||
'@mdx-js/rollup': 2.3.0(rollup@3.28.0)
|
||||
acorn: 8.8.2
|
||||
es-module-lexer: 1.2.1
|
||||
estree-util-visit: 1.2.1
|
||||
@ -2438,6 +2458,15 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm64@0.18.20:
|
||||
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm@0.17.18:
|
||||
@ -2446,6 +2475,15 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm@0.18.20:
|
||||
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-x64@0.17.18:
|
||||
@ -2454,6 +2492,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-x64@0.18.20:
|
||||
resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-arm64@0.17.18:
|
||||
@ -2462,6 +2509,15 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-arm64@0.18.20:
|
||||
resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-x64@0.17.18:
|
||||
@ -2470,6 +2526,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-x64@0.18.20:
|
||||
resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-arm64@0.17.18:
|
||||
@ -2478,6 +2543,15 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-arm64@0.18.20:
|
||||
resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-x64@0.17.18:
|
||||
@ -2486,6 +2560,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-x64@0.18.20:
|
||||
resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm64@0.17.18:
|
||||
@ -2494,6 +2577,15 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm64@0.18.20:
|
||||
resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm@0.17.18:
|
||||
@ -2502,6 +2594,15 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm@0.18.20:
|
||||
resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ia32@0.17.18:
|
||||
@ -2510,6 +2611,15 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ia32@0.18.20:
|
||||
resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-loong64@0.17.18:
|
||||
@ -2518,6 +2628,15 @@ packages:
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-loong64@0.18.20:
|
||||
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-mips64el@0.17.18:
|
||||
@ -2526,6 +2645,15 @@ packages:
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-mips64el@0.18.20:
|
||||
resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ppc64@0.17.18:
|
||||
@ -2534,6 +2662,15 @@ packages:
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ppc64@0.18.20:
|
||||
resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-riscv64@0.17.18:
|
||||
@ -2542,6 +2679,15 @@ packages:
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-riscv64@0.18.20:
|
||||
resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-s390x@0.17.18:
|
||||
@ -2550,6 +2696,15 @@ packages:
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-s390x@0.18.20:
|
||||
resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-x64@0.17.18:
|
||||
@ -2558,6 +2713,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-x64@0.18.20:
|
||||
resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/netbsd-x64@0.17.18:
|
||||
@ -2566,6 +2730,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/netbsd-x64@0.18.20:
|
||||
resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/openbsd-x64@0.17.18:
|
||||
@ -2574,6 +2747,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/openbsd-x64@0.18.20:
|
||||
resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/sunos-x64@0.17.18:
|
||||
@ -2582,6 +2764,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/sunos-x64@0.18.20:
|
||||
resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-arm64@0.17.18:
|
||||
@ -2590,6 +2781,15 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-arm64@0.18.20:
|
||||
resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-ia32@0.17.18:
|
||||
@ -2598,6 +2798,15 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-ia32@0.18.20:
|
||||
resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-x64@0.17.18:
|
||||
@ -2606,6 +2815,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-x64@0.18.20:
|
||||
resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/@eslint-community/eslint-utils@4.4.0(eslint@8.39.0):
|
||||
@ -2927,14 +3145,14 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@mdx-js/rollup@2.3.0(rollup@3.21.0):
|
||||
/@mdx-js/rollup@2.3.0(rollup@3.28.0):
|
||||
resolution: {integrity: sha512-wLvRfJS/M4UmdqTd+WoaySEE7q4BIejYf1xAHXYvtT1du/1Tl/z2450Gg2+Hu7fh05KwRRiehiTP9Yc/Dtn0fA==}
|
||||
peerDependencies:
|
||||
rollup: '>=2'
|
||||
dependencies:
|
||||
'@mdx-js/mdx': 2.3.0
|
||||
'@rollup/pluginutils': 5.0.2(rollup@3.21.0)
|
||||
rollup: 3.21.0
|
||||
'@rollup/pluginutils': 5.0.2(rollup@3.28.0)
|
||||
rollup: 3.28.0
|
||||
source-map: 0.7.4
|
||||
vfile: 5.3.6
|
||||
transitivePeerDependencies:
|
||||
@ -3612,7 +3830,7 @@ packages:
|
||||
rollup: 3.12.0
|
||||
dev: true
|
||||
|
||||
/@rollup/pluginutils@5.0.2(rollup@3.21.0):
|
||||
/@rollup/pluginutils@5.0.2(rollup@3.28.0):
|
||||
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
@ -3624,7 +3842,7 @@ packages:
|
||||
'@types/estree': 1.0.0
|
||||
estree-walker: 2.0.2
|
||||
picomatch: 2.3.1
|
||||
rollup: 3.21.0
|
||||
rollup: 3.28.0
|
||||
dev: false
|
||||
|
||||
/@sigstore/protobuf-specs@0.1.0:
|
||||
@ -4554,7 +4772,7 @@ packages:
|
||||
vite-plugin-pwa: ^0.14.0
|
||||
dependencies:
|
||||
astro: 2.3.2(@types/node@18.16.3)
|
||||
vite-plugin-pwa: 0.14.7(vite@4.3.3)(workbox-build@6.5.4)(workbox-window@6.5.4)
|
||||
vite-plugin-pwa: 0.14.7(vite@4.4.5)(workbox-build@6.5.4)(workbox-window@6.5.4)
|
||||
dev: true
|
||||
|
||||
/@vitejs/plugin-react@4.0.0(vite@4.3.3):
|
||||
@ -5036,13 +5254,14 @@ packages:
|
||||
typescript: 4.9.4
|
||||
unist-util-visit: 4.1.2
|
||||
vfile: 5.3.6
|
||||
vite: 4.3.3(@types/node@18.16.3)
|
||||
vitefu: 0.2.4(vite@4.3.3)
|
||||
vite: 4.4.5(@types/node@18.16.3)
|
||||
vitefu: 0.2.4(vite@4.4.5)
|
||||
yargs-parser: 21.1.1
|
||||
zod: 3.21.4
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- stylus
|
||||
- sugarss
|
||||
@ -6478,6 +6697,36 @@ packages:
|
||||
'@esbuild/win32-arm64': 0.17.18
|
||||
'@esbuild/win32-ia32': 0.17.18
|
||||
'@esbuild/win32-x64': 0.17.18
|
||||
dev: true
|
||||
|
||||
/esbuild@0.18.20:
|
||||
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
'@esbuild/android-arm': 0.18.20
|
||||
'@esbuild/android-arm64': 0.18.20
|
||||
'@esbuild/android-x64': 0.18.20
|
||||
'@esbuild/darwin-arm64': 0.18.20
|
||||
'@esbuild/darwin-x64': 0.18.20
|
||||
'@esbuild/freebsd-arm64': 0.18.20
|
||||
'@esbuild/freebsd-x64': 0.18.20
|
||||
'@esbuild/linux-arm': 0.18.20
|
||||
'@esbuild/linux-arm64': 0.18.20
|
||||
'@esbuild/linux-ia32': 0.18.20
|
||||
'@esbuild/linux-loong64': 0.18.20
|
||||
'@esbuild/linux-mips64el': 0.18.20
|
||||
'@esbuild/linux-ppc64': 0.18.20
|
||||
'@esbuild/linux-riscv64': 0.18.20
|
||||
'@esbuild/linux-s390x': 0.18.20
|
||||
'@esbuild/linux-x64': 0.18.20
|
||||
'@esbuild/netbsd-x64': 0.18.20
|
||||
'@esbuild/openbsd-x64': 0.18.20
|
||||
'@esbuild/sunos-x64': 0.18.20
|
||||
'@esbuild/win32-arm64': 0.18.20
|
||||
'@esbuild/win32-ia32': 0.18.20
|
||||
'@esbuild/win32-x64': 0.18.20
|
||||
|
||||
/escalade@3.1.1:
|
||||
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
|
||||
@ -10854,6 +11103,14 @@ packages:
|
||||
picocolors: 1.0.0
|
||||
source-map-js: 1.0.2
|
||||
|
||||
/postcss@8.4.27:
|
||||
resolution: {integrity: sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
dependencies:
|
||||
nanoid: 3.3.6
|
||||
picocolors: 1.0.0
|
||||
source-map-js: 1.0.2
|
||||
|
||||
/prebuild-install@7.1.1:
|
||||
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
|
||||
engines: {node: '>=10'}
|
||||
@ -11658,6 +11915,14 @@ packages:
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/rollup@3.28.0:
|
||||
resolution: {integrity: sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw==}
|
||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
/run-async@2.4.1:
|
||||
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
|
||||
@ -13063,10 +13328,11 @@ packages:
|
||||
mlly: 1.4.0
|
||||
pathe: 1.1.1
|
||||
picocolors: 1.0.0
|
||||
vite: 4.3.3(@types/node@18.16.3)
|
||||
vite: 4.4.5(@types/node@18.16.3)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- stylus
|
||||
- sugarss
|
||||
@ -13074,7 +13340,7 @@ packages:
|
||||
- terser
|
||||
dev: true
|
||||
|
||||
/vite-plugin-pwa@0.14.7(vite@4.3.3)(workbox-build@6.5.4)(workbox-window@6.5.4):
|
||||
/vite-plugin-pwa@0.14.7(vite@4.4.5)(workbox-build@6.5.4)(workbox-window@6.5.4):
|
||||
resolution: {integrity: sha512-dNJaf0fYOWncmjxv9HiSa2xrSjipjff7IkYE5oIUJ2x5HKu3cXgA8LRgzOwTc5MhwyFYRSU0xyN0Phbx3NsQYw==}
|
||||
peerDependencies:
|
||||
vite: ^3.1.0 || ^4.0.0
|
||||
@ -13086,7 +13352,7 @@ packages:
|
||||
fast-glob: 3.2.12
|
||||
pretty-bytes: 6.1.0
|
||||
rollup: 3.12.0
|
||||
vite: 4.3.3(@types/node@18.16.3)
|
||||
vite: 4.4.5(@types/node@18.16.3)
|
||||
workbox-build: 6.5.4
|
||||
workbox-window: 6.5.4
|
||||
transitivePeerDependencies:
|
||||
@ -13124,8 +13390,44 @@ packages:
|
||||
rollup: 3.21.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/vitefu@0.2.4(vite@4.3.3):
|
||||
/vite@4.4.5(@types/node@18.16.3):
|
||||
resolution: {integrity: sha512-4m5kEtAWHYr0O1Fu7rZp64CfO1PsRGZlD3TAB32UmQlpd7qg15VF7ROqGN5CyqN7HFuwr7ICNM2+fDWRqFEKaA==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': '>= 14'
|
||||
less: '*'
|
||||
lightningcss: ^1.21.0
|
||||
sass: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.4.0
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 18.16.3
|
||||
esbuild: 0.18.20
|
||||
postcss: 8.4.27
|
||||
rollup: 3.28.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
/vitefu@0.2.4(vite@4.4.5):
|
||||
resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==}
|
||||
peerDependencies:
|
||||
vite: ^3.0.0 || ^4.0.0
|
||||
@ -13133,7 +13435,7 @@ packages:
|
||||
vite:
|
||||
optional: true
|
||||
dependencies:
|
||||
vite: 4.3.3(@types/node@18.16.3)
|
||||
vite: 4.4.5(@types/node@18.16.3)
|
||||
|
||||
/vitest@0.33.0(@vitest/ui@0.28.0):
|
||||
resolution: {integrity: sha512-1CxaugJ50xskkQ0e969R/hW47za4YXDUfWJDxip1hwbnhUjYolpfUn2AMOulqG/Dtd9WYAtkHmM/m3yKVrEejQ==}
|
||||
@ -13193,6 +13495,7 @@ packages:
|
||||
why-is-node-running: 2.2.2
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- stylus
|
||||
- sugarss
|
||||
|
||||
@ -6,3 +6,4 @@ packages:
|
||||
- "packages/core/examples/vite-vanilla-repl-cm6"
|
||||
- "packages/react/examples/nano-repl"
|
||||
- "packages/web/examples/repl-example"
|
||||
- "packages/superdough/example"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user