Merge branch 'master' into add-groups-to-command-palette

This commit is contained in:
Jan Faracik 2024-12-16 20:53:15 +00:00
commit 428e826fcd
14 changed files with 283 additions and 147 deletions

View File

@ -70,7 +70,7 @@ THE SOFTWARE.
<!-- https://docs.spring.io/spring-security/reference/6.3/getting-spring-security.html#getting-maven-no-boot -->
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-bom</artifactId>
<version>6.4.1</version>
<version>6.4.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>

View File

@ -58,6 +58,8 @@ import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import jenkins.model.Jenkins;
import jenkins.security.MasterToSlaveCallable;
import jenkins.security.ResourceDomainConfiguration;
@ -65,8 +67,6 @@ import jenkins.security.ResourceDomainRootAction;
import jenkins.util.SystemProperties;
import jenkins.util.VirtualFile;
import org.apache.commons.io.IOUtils;
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipOutputStream;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponse;
@ -530,8 +530,8 @@ public final class DirectoryBrowserSupport implements HttpResponse {
private static void zip(StaplerResponse2 rsp, VirtualFile root, VirtualFile dir, String glob) throws IOException, InterruptedException {
OutputStream outputStream = rsp.getOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(outputStream)) {
zos.setEncoding(Charset.defaultCharset().displayName()); // TODO JENKINS-20663 make this overridable via query parameter
// TODO JENKINS-20663 make encoding overridable via query parameter
try (ZipOutputStream zos = new ZipOutputStream(outputStream, Charset.defaultCharset())) {
// TODO consider using run(Callable) here
if (glob.isEmpty()) {

View File

@ -4,6 +4,8 @@ import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.Util;
import hudson.model.Job;
import hudson.model.PermalinkProjectAction.Permalink;
@ -14,6 +16,7 @@ import hudson.model.listeners.RunListener;
import hudson.util.AtomicFileWriter;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashMap;
@ -24,6 +27,7 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
@ -63,14 +67,6 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
*/
public abstract class PeepholePermalink extends Permalink implements Predicate<Run<?, ?>> {
/**
* JENKINS-22822: avoids rereading caches.
* Top map keys are {@code builds} directories.
* Inner maps are from permalink name to build number.
* Synchronization is first on the outer map, then on the inner.
*/
private static final Map<File, Map<String, Integer>> caches = new HashMap<>();
/**
* Checks if the given build satisfies the peep-hole criteria.
*
@ -94,54 +90,181 @@ public abstract class PeepholePermalink extends Permalink implements Predicate<R
*/
@Override
public Run<?, ?> resolve(Job<?, ?> job) {
Map<String, Integer> cache = cacheFor(job.getBuildDir());
int n;
synchronized (cache) {
n = cache.getOrDefault(getId(), 0);
}
if (n == RESOLVES_TO_NONE) {
return null;
}
Run<?, ?> b;
if (n > 0) {
b = job.getBuildByNumber(n);
if (b != null && apply(b)) {
return b; // found it (in the most efficient way possible)
}
} else {
b = null;
}
// the cache is stale. start the search
if (b == null) {
b = job.getNearestOldBuild(n);
}
if (b == null) {
// no cache
b = job.getLastBuild();
}
// start from the build 'b' and locate the build that matches the criteria going back in time
b = find(b);
updateCache(job, b);
return b;
return ExtensionList.lookupFirst(Cache.class).get(job, getId()).resolve(this, job, getId());
}
/**
* Start from the build 'b' and locate the build that matches the criteria going back in time
*/
private Run<?, ?> find(Run<?, ?> b) {
//noinspection StatementWithEmptyBody
for ( ; b != null && !apply(b); b = b.getPreviousBuild())
;
@CheckForNull
private Run<?, ?> find(@CheckForNull Run<?, ?> b) {
while (b != null && !apply(b)) {
b = b.getPreviousBuild();
}
return b;
}
private static @NonNull Map<String, Integer> cacheFor(@NonNull File buildDir) {
/**
* Remembers the value 'n' in the cache for future {@link #resolve(Job)}.
*/
protected void updateCache(@NonNull Job<?, ?> job, @CheckForNull Run<?, ?> b) {
ExtensionList.lookupFirst(Cache.class).put(job, getId(), b != null ? new Cache.Some(b.getNumber()) : Cache.NONE);
}
/**
* Persistable cache of peephole permalink targets.
*/
@Restricted(Beta.class)
public interface Cache extends ExtensionPoint {
/** Cacheable target of a permalink. */
sealed interface PermalinkTarget extends Serializable {
/**
* Implementation of {@link #resolve(Job)}.
* This may update the cache if it was missing or found to be invalid.
*/
@Restricted(NoExternalUse.class)
@CheckForNull
Run<?, ?> resolve(@NonNull PeepholePermalink pp, @NonNull Job<?, ?> job, @NonNull String id);
/**
* Partial implementation of {@link #resolve(PeepholePermalink, Job, String)} when searching.
* @param b if set, the newest build to even consider when searching
*/
@Restricted(NoExternalUse.class)
@CheckForNull
default Run<?, ?> search(@NonNull PeepholePermalink pp, @NonNull Job<?, ?> job, @NonNull String id, @CheckForNull Run<?, ?> b) {
if (b == null) {
// no cache
b = job.getLastBuild();
}
// start from the build 'b' and locate the build that matches the criteria going back in time
b = pp.find(b);
pp.updateCache(job, b);
return b;
}
}
/**
* The cache entry for this target is missing.
*/
record Unknown() implements PermalinkTarget {
@Override
public Run<?, ?> resolve(PeepholePermalink pp, Job<?, ?> job, String id) {
return search(pp, job, id, null);
}
}
Unknown UNKNOWN = new Unknown();
/**
* The cache entry for this target is present.
*/
sealed interface Known extends PermalinkTarget {}
/** There is known to be no matching build. */
record None() implements Known {
@Override
public Run<?, ?> resolve(PeepholePermalink pp, Job<?, ?> job, String id) {
return null;
}
}
/** Singleton of {@link None}. */
None NONE = new None();
/** A matching build, indicated by {@link Run#getNumber}. */
record Some(int number) implements Known {
@Override
public Run<?, ?> resolve(PeepholePermalink pp, Job<?, ?> job, String id) {
Run<?, ?> b = job.getBuildByNumber(number);
if (b != null && pp.apply(b)) {
return b; // found it (in the most efficient way possible)
}
// the cache is stale. start the search
if (b == null) {
b = job.getNearestOldBuild(number);
}
return search(pp, job, id, b);
}
}
/**
* Looks for any existing cache hit.
* @param id {@link #getId}
* @return {@link Some} or {@link #NONE} or {@link #UNKNOWN}
*/
@NonNull PermalinkTarget get(@NonNull Job<?, ?> job, @NonNull String id);
/**
* Updates the cache.
* Note that this may be called not just when a build completes or is deleted
* (meaning that the logical value of the cache has changed),
* but also when {@link #resolve} has failed to find a cached value
* (or determined that a previously cached value is in fact invalid).
* @param id {@link #getId}
* @param target {@link Some} or {@link #NONE}
*/
void put(@NonNull Job<?, ?> job, @NonNull String id, @NonNull Known target);
}
/**
* Default cache based on a {@code permalinks} file in the build directory.
* There is one line per cached permalink, in the format {@code lastStableBuild 123}
* or (for a negative cache) {@code lastFailedBuild -1}.
*/
@Restricted(NoExternalUse.class)
@Extension(ordinal = -1000)
public static final class DefaultCache implements Cache {
/**
* JENKINS-22822: avoids rereading caches.
* Top map keys are {@code builds} directories.
* Inner maps are from permalink name to target.
* Synchronization is first on the outer map, then on the inner.
*/
private final Map<File, Map<String, Known>> caches = new HashMap<>();
@Override
public PermalinkTarget get(Job<?, ?> job, String id) {
var cache = cacheFor(job.getBuildDir());
synchronized (cache) {
var cached = cache.get(id);
return cached != null ? cached : UNKNOWN;
}
}
@Override
public void put(Job<?, ?> job, String id, Known target) {
File buildDir = job.getBuildDir();
var cache = cacheFor(buildDir);
synchronized (cache) {
cache.put(id, target);
File storage = storageFor(buildDir);
LOGGER.fine(() -> "saving to " + storage + ": " + cache);
try (AtomicFileWriter cw = new AtomicFileWriter(storage)) {
try {
for (var entry : cache.entrySet()) {
cw.write(entry.getKey());
cw.write(' ');
cw.write(Integer.toString(entry.getValue() instanceof Cache.Some some ? some.number : -1));
cw.write('\n');
}
cw.commit();
} finally {
cw.abort();
}
} catch (IOException x) {
LOGGER.log(Level.WARNING, "failed to update " + storage, x);
}
}
}
private @NonNull Map<String, Known> cacheFor(@NonNull File buildDir) {
synchronized (caches) {
Map<String, Integer> cache = caches.get(buildDir);
var cache = caches.get(buildDir);
if (cache == null) {
cache = load(buildDir);
caches.put(buildDir, cache);
@ -150,8 +273,8 @@ public abstract class PeepholePermalink extends Permalink implements Predicate<R
}
}
private static @NonNull Map<String, Integer> load(@NonNull File buildDir) {
Map<String, Integer> cache = new TreeMap<>();
private static @NonNull Map<String, Known> load(@NonNull File buildDir) {
Map<String, Known> cache = new TreeMap<>();
File storage = storageFor(buildDir);
if (storage.isFile()) {
try (Stream<String> lines = Files.lines(storage.toPath(), StandardCharsets.UTF_8)) {
@ -161,7 +284,8 @@ public abstract class PeepholePermalink extends Permalink implements Predicate<R
return;
}
try {
cache.put(line.substring(0, idx), Integer.parseInt(line.substring(idx + 1)));
int number = Integer.parseInt(line.substring(idx + 1));
cache.put(line.substring(0, idx), number == -1 ? Cache.NONE : new Cache.Some(number));
} catch (NumberFormatException x) {
LOGGER.log(Level.WARNING, "failed to read " + storage, x);
}
@ -177,33 +301,6 @@ public abstract class PeepholePermalink extends Permalink implements Predicate<R
static @NonNull File storageFor(@NonNull File buildDir) {
return new File(buildDir, "permalinks");
}
/**
* Remembers the value 'n' in the cache for future {@link #resolve(Job)}.
*/
protected void updateCache(@NonNull Job<?, ?> job, @CheckForNull Run<?, ?> b) {
File buildDir = job.getBuildDir();
Map<String, Integer> cache = cacheFor(buildDir);
synchronized (cache) {
cache.put(getId(), b == null ? RESOLVES_TO_NONE : b.getNumber());
File storage = storageFor(buildDir);
LOGGER.fine(() -> "saving to " + storage + ": " + cache);
try (AtomicFileWriter cw = new AtomicFileWriter(storage)) {
try {
for (Map.Entry<String, Integer> entry : cache.entrySet()) {
cw.write(entry.getKey());
cw.write(' ');
cw.write(Integer.toString(entry.getValue()));
cw.write('\n');
}
cw.commit();
} finally {
cw.abort();
}
} catch (IOException x) {
LOGGER.log(Level.WARNING, "failed to update " + storage, x);
}
}
}
@Extension
@ -380,7 +477,5 @@ public abstract class PeepholePermalink extends Permalink implements Predicate<R
@Restricted(NoExternalUse.class)
public static void initialized() {}
private static final int RESOLVES_TO_NONE = -1;
private static final Logger LOGGER = Logger.getLogger(PeepholePermalink.class.getName());
}

View File

@ -60,6 +60,8 @@ import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import jenkins.MasterToSlaveFileCallable;
import jenkins.model.ArtifactManager;
import jenkins.security.MasterToSlaveCallable;
@ -68,8 +70,6 @@ import org.apache.tools.ant.types.AbstractFileSet;
import org.apache.tools.ant.types.selectors.SelectorUtils;
import org.apache.tools.ant.types.selectors.TokenizedPath;
import org.apache.tools.ant.types.selectors.TokenizedPattern;
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipOutputStream;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@ -375,8 +375,8 @@ public abstract class VirtualFile implements Comparable<VirtualFile>, Serializab
}
Collection<String> files = list(includes, excludes, useDefaultExcludes, openOptions);
try (ZipOutputStream zos = new ZipOutputStream(outputStream)) {
zos.setEncoding(Charset.defaultCharset().displayName()); // TODO JENKINS-20663 make this overridable via query parameter
// TODO JENKINS-20663 make encoding overridable via query parameter
try (ZipOutputStream zos = new ZipOutputStream(outputStream, Charset.defaultCharset())) {
for (String relativePath : files) {
VirtualFile virtualFile = this.child(relativePath);

View File

@ -39,10 +39,10 @@
"mini-css-extract-plugin": "2.9.2",
"postcss": "8.4.49",
"postcss-loader": "8.1.1",
"postcss-preset-env": "10.1.1",
"postcss-preset-env": "10.1.2",
"postcss-scss": "4.0.9",
"prettier": "3.4.2",
"sass": "1.82.0",
"sass": "1.83.0",
"sass-loader": "16.0.4",
"style-loader": "4.0.0",
"stylelint": "16.11.0",
@ -54,7 +54,7 @@
},
"dependencies": {
"handlebars": "4.7.8",
"hotkeys-js": "3.12.2",
"hotkeys-js": "3.13.9",
"jquery": "3.7.1",
"lodash": "4.17.21",
"sortablejs": "1.15.6",

View File

@ -97,7 +97,7 @@ THE SOFTWARE.
<bridge-method-injector.version>1.30</bridge-method-injector.version>
<spotless.check.skip>false</spotless.check.skip>
<!-- Make sure to keep the jetty-ee9-maven-plugin version in war/pom.xml in sync with the Jetty release in Winstone: -->
<winstone.version>8.2</winstone.version>
<winstone.version>8.4</winstone.version>
<node.version>20.18.1</node.version>
</properties>

View File

@ -33,6 +33,8 @@ function init() {
searchResultsContainer,
() => searchResults.querySelectorAll("a"),
hoverClass,
() => {},
() => commandPalette.open,
);
// Events

View File

@ -81,7 +81,7 @@ export default function makeKeyboardNavigable(
}
function scrollAndSelect(selectedItem, selectedClass, items) {
if (selectedItem !== null) {
if (selectedItem) {
if (!isInViewport(selectedItem)) {
selectedItem.scrollIntoView(false);
}

View File

@ -218,7 +218,7 @@ THE SOFTWARE.
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>cloudbees-folder</artifactId>
<version>6.973.vc9b_85a_61e4fc</version>
<version>6.975.v4161e479479f</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@ -664,6 +664,8 @@ public class AbstractProjectTest {
JSONObject o = new JSONObject();
o.put("name", p);
o.put("url", JSONObject.fromObject(null));
o.put("icon", JSONObject.fromObject(null));
o.put("type", "symbol");
expected.add(o);
}
assertThat(suggestions.containsAll(expected), is(true));

View File

@ -82,7 +82,7 @@ public class PeepholePermalinkTest {
}
private void assertStorage(String id, Job<?, ?> job, Run<?, ?> build) throws Exception {
assertThat(Files.readAllLines(PeepholePermalink.storageFor(job.getBuildDir()).toPath(), StandardCharsets.UTF_8),
assertThat(Files.readAllLines(PeepholePermalink.DefaultCache.storageFor(job.getBuildDir()).toPath(), StandardCharsets.UTF_8),
hasItem(id + " " + (build == null ? -1 : build.getNumber())));
}

View File

@ -24,6 +24,9 @@
package jenkins.model.lazy;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
@ -87,6 +90,40 @@ public class LazyBuildMixInTest {
assertEquals(b3, b1a.getNextBuild());
}
@Test public void numericLookups() throws Exception {
var p = r.createFreeStyleProject();
var b1 = r.buildAndAssertSuccess(p);
var b2 = r.buildAndAssertSuccess(p);
var b3 = r.buildAndAssertSuccess(p);
var b4 = r.buildAndAssertSuccess(p);
var b5 = r.buildAndAssertSuccess(p);
var b6 = r.buildAndAssertSuccess(p);
b1.delete();
b3.delete();
b6.delete();
// leaving 2, 4, 5
assertThat(p.getFirstBuild(), is(b2));
assertThat(p.getLastBuild(), is(b5));
assertThat(p.getNearestBuild(-1), is(b2));
assertThat(p.getNearestBuild(0), is(b2));
assertThat(p.getNearestBuild(1), is(b2));
assertThat(p.getNearestBuild(2), is(b2));
assertThat(p.getNearestBuild(3), is(b4));
assertThat(p.getNearestBuild(4), is(b4));
assertThat(p.getNearestBuild(5), is(b5));
assertThat(p.getNearestBuild(6), nullValue());
assertThat(p.getNearestBuild(7), nullValue());
assertThat(p.getNearestOldBuild(-1), nullValue());
assertThat(p.getNearestOldBuild(0), nullValue());
assertThat(p.getNearestOldBuild(1), nullValue());
assertThat(p.getNearestOldBuild(2), is(b2));
assertThat(p.getNearestOldBuild(3), is(b2));
assertThat(p.getNearestOldBuild(4), is(b4));
assertThat(p.getNearestOldBuild(5), is(b5));
assertThat(p.getNearestOldBuild(6), is(b5));
assertThat(p.getNearestOldBuild(7), is(b5));
}
@Issue("JENKINS-20662")
@Test public void newRunningBuildRelationFromPrevious() throws Exception {
FreeStyleProject p = r.createFreeStyleProject();

View File

@ -645,7 +645,7 @@ THE SOFTWARE.
<plugin>
<groupId>org.eclipse.jetty.ee9</groupId>
<artifactId>jetty-ee9-maven-plugin</artifactId>
<version>12.0.14</version>
<version>12.0.16</version>
<configuration>
<!--
Reload webapp when you hit ENTER. (See JETTY-282 for more)

View File

@ -3040,16 +3040,16 @@ __metadata:
languageName: node
linkType: hard
"css-has-pseudo@npm:^7.0.1":
version: 7.0.1
resolution: "css-has-pseudo@npm:7.0.1"
"css-has-pseudo@npm:^7.0.2":
version: 7.0.2
resolution: "css-has-pseudo@npm:7.0.2"
dependencies:
"@csstools/selector-specificity": "npm:^5.0.0"
postcss-selector-parser: "npm:^7.0.0"
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4
checksum: 10c0/13789b08b70169204be786d652190356ace9313099d3656bd2fc38afbdd28f3d9620f0e0b07425480961b7a1ec789794961d0472f205b959d3f64c9a78ce511c
checksum: 10c0/456e9ce1eec8a535683c329956acfe53ce5a208345d7f2fcbe662626be8b3c98681e9041d7f4980316714397b0c1c3defde25653d629c396df17803d599c4edf
languageName: node
linkType: hard
@ -3165,10 +3165,10 @@ __metadata:
languageName: node
linkType: hard
"cssdb@npm:^8.2.1":
version: 8.2.1
resolution: "cssdb@npm:8.2.1"
checksum: 10c0/d27d7db0a39e1105181aac119a98d6c92cd5ceba2e8bd349cdf2ba4a8d9ead149b685a1dba9542ca24f094cc70eca4a3e02973fe1f74c11a373b508606e5e1c0
"cssdb@npm:^8.2.3":
version: 8.2.3
resolution: "cssdb@npm:8.2.3"
checksum: 10c0/17c3ca6432ed02431db6b44bed74649ccef7d7b7b900ccbc7297525f030722c441dd67c71f28aef3cfa0814ba7b254a24adfb0dcd5728937da179ff437cdcd0c
languageName: node
linkType: hard
@ -4071,10 +4071,10 @@ __metadata:
languageName: node
linkType: hard
"hotkeys-js@npm:3.12.2":
version: 3.12.2
resolution: "hotkeys-js@npm:3.12.2"
checksum: 10c0/67f19a01de9d1a6ad4ce1055734a7adc0a52fef81ccb1f61f4930a58ad93fca9c382f16647ef0abd9e1610814caddb0d3ca6ce7a8e6fcc43e26275423de617a9
"hotkeys-js@npm:3.13.9":
version: 3.13.9
resolution: "hotkeys-js@npm:3.13.9"
checksum: 10c0/83511e7e812395289231605f6c50974f85c270de0316b21ffc4b794d29f2a1aa927a29644c77f1f77ac6e3228fa91fb4bc5f01fb5b27e00196af4d8c5a4a6693
languageName: node
linkType: hard
@ -4383,16 +4383,16 @@ __metadata:
globals: "npm:15.13.0"
handlebars: "npm:4.7.8"
handlebars-loader: "npm:1.7.3"
hotkeys-js: "npm:3.12.2"
hotkeys-js: "npm:3.13.9"
jquery: "npm:3.7.1"
lodash: "npm:4.17.21"
mini-css-extract-plugin: "npm:2.9.2"
postcss: "npm:8.4.49"
postcss-loader: "npm:8.1.1"
postcss-preset-env: "npm:10.1.1"
postcss-preset-env: "npm:10.1.2"
postcss-scss: "npm:4.0.9"
prettier: "npm:3.4.2"
sass: "npm:1.82.0"
sass: "npm:1.83.0"
sass-loader: "npm:16.0.4"
sortablejs: "npm:1.15.6"
style-loader: "npm:4.0.0"
@ -5893,9 +5893,9 @@ __metadata:
languageName: node
linkType: hard
"postcss-preset-env@npm:10.1.1":
version: 10.1.1
resolution: "postcss-preset-env@npm:10.1.1"
"postcss-preset-env@npm:10.1.2":
version: 10.1.2
resolution: "postcss-preset-env@npm:10.1.2"
dependencies:
"@csstools/postcss-cascade-layers": "npm:^5.0.1"
"@csstools/postcss-color-function": "npm:^4.0.6"
@ -5932,9 +5932,9 @@ __metadata:
autoprefixer: "npm:^10.4.19"
browserslist: "npm:^4.23.1"
css-blank-pseudo: "npm:^7.0.1"
css-has-pseudo: "npm:^7.0.1"
css-has-pseudo: "npm:^7.0.2"
css-prefers-color-scheme: "npm:^10.0.0"
cssdb: "npm:^8.2.1"
cssdb: "npm:^8.2.3"
postcss-attribute-case-insensitive: "npm:^7.0.1"
postcss-clamp: "npm:^4.1.0"
postcss-color-functional-notation: "npm:^7.0.6"
@ -5962,7 +5962,7 @@ __metadata:
postcss-selector-not: "npm:^8.0.1"
peerDependencies:
postcss: ^8.4
checksum: 10c0/99931117735a66827c7318be023ddb614990457617ccbe7fd2fdc1f10345554652df180d4842768d68d57e14fc0be4d86d0b413c65e77e02db5511e57ed07c4f
checksum: 10c0/bd40330867a525679d434ff9602efbf229da9e745a3759c2d0b3cff166dd0f17bf99b44673ebb316df1906c2bf2edef97aeef1840aa6be170b43a34404df396d
languageName: node
linkType: hard
@ -6381,9 +6381,9 @@ __metadata:
languageName: node
linkType: hard
"sass@npm:1.82.0":
version: 1.82.0
resolution: "sass@npm:1.82.0"
"sass@npm:1.83.0":
version: 1.83.0
resolution: "sass@npm:1.83.0"
dependencies:
"@parcel/watcher": "npm:^2.4.1"
chokidar: "npm:^4.0.0"
@ -6394,7 +6394,7 @@ __metadata:
optional: true
bin:
sass: sass.js
checksum: 10c0/7f86fe6ade4f6018862c448ed69d5c52f485b0125c9dab24e63f679739a04cc7c56562d588e3cf16b5efb4d2c4d0530e62740e1cfd273e2e3707d04d11011736
checksum: 10c0/4415361229879a9041d77c953da85482e89032aa4321ba13250a9987d39c80fac6c88af3777f2a2d76a4e8b0c8afbd21c1970fdbe84e0b3ec25fb26741f92beb
languageName: node
linkType: hard