@scratch/scratch-vm 13.7.3 → 13.7.4-svg

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scratch/scratch-vm",
3
- "version": "13.7.3",
3
+ "version": "13.7.4-svg",
4
4
  "description": "Virtual Machine for Scratch 3.0",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/scratchfoundation/scratch-vm#readme",
@@ -60,8 +60,8 @@
60
60
  "allow-incomplete-coverage": true
61
61
  },
62
62
  "dependencies": {
63
- "@scratch/scratch-render": "13.7.3",
64
- "@scratch/scratch-svg-renderer": "13.7.3",
63
+ "@scratch/scratch-render": "13.7.4-svg",
64
+ "@scratch/scratch-svg-renderer": "13.7.4-svg",
65
65
  "@vernier/godirect": "1.8.3",
66
66
  "arraybuffer-loader": "1.0.8",
67
67
  "atob": "2.1.2",
@@ -654,38 +654,52 @@ class Target extends EventEmitter {
654
654
  }
655
655
 
656
656
  /**
657
- * Reconciles variable, list, and broadcast references on this target against
658
- * the variables actually defined in the project, creating definitions on the
659
- * stage for any references whose ids are not defined anywhere. Does not
660
- * rename any existing variables.
657
+ * Fixes up variable references in this target avoiding conflicts with
658
+ * pre-existing variables in the same scope.
659
+ * This is used when uploading this target as a new sprite into an existing
660
+ * project, where the new sprite may contain references
661
+ * to variable names that already exist as global variables in the project
662
+ * (and thus are in scope for variable references in the given sprite).
661
663
  *
662
- * For each variable, list, or broadcast referenced by a block on this target:
663
- * - If the referenced id is found locally on this sprite or on the stage,
664
- * the reference is already correct and nothing happens.
665
- * - If the referenced id is not defined anywhere, the stage is checked for a
666
- * variable with the same name and type. If one exists, the field id is
667
- * remapped to that existing global. Otherwise, a new global is created on
668
- * the stage with a non-conflicting name and the field name is updated.
664
+ * If this target has a block that references an existing global variable and that
665
+ * variable *does not* exist in this target (e.g. it was a global variable in the
666
+ * project the sprite was originally exported from), merge the variables. This entails
667
+ * fixing the variable references in this sprite to reference the id of the pre-existing global variable.
669
668
  *
670
- * Used during whole-project load to repair projects corrupted by historical
671
- * bugs that left dangling references, and as the definition-creation phase
672
- * of `fixUpVariableReferences` for sprite import and backpack paste.
669
+ * If this target has a block that references an existing global variable and that
670
+ * variable does exist in the target itself (e.g. it's a local variable in the sprite being uploaded),
671
+ * then the local variable is renamed to distinguish itself from the pre-existing variable.
672
+ * All blocks that reference the local variable will be updated to use the new name.
673
673
  */
674
- reconcileVariableReferences () {
675
- if (!this.runtime) return;
674
+ // TODO (#1360) This function is too long, add some helpers for the different chunks and cases...
675
+ fixUpVariableReferences () {
676
+ if (!this.runtime) return; // There's no runtime context to conflict with
677
+ if (this.isStage) return; // Stage can't have variable conflicts with itself (and also can't be uploaded)
676
678
  const stage = this.runtime.getTargetForStage();
677
679
  if (!stage || !stage.variables) return;
678
680
 
679
- const allReferences = this.blocks.getAllVariableAndListReferences(null, true);
681
+ const renameConflictingLocalVar = (id, name, type) => {
682
+ const conflict = stage.lookupVariableByNameAndType(name, type);
683
+ if (conflict) {
684
+ const newName = StringUtil.unusedName(
685
+ `${this.getName()}: ${name}`,
686
+ this.getAllVariableNamesInScopeByType(type));
687
+ this.renameVariable(id, newName);
688
+ return newName;
689
+ }
690
+ return null;
691
+ };
692
+
693
+ const allReferences = this.blocks.getAllVariableAndListReferences();
694
+ const unreferencedLocalVarIds = [];
695
+ if (Object.keys(this.variables).length > 0) {
696
+ for (const localVarId in this.variables) {
697
+ if (!Object.prototype.hasOwnProperty.call(this.variables, localVarId)) continue;
698
+ if (!allReferences[localVarId]) unreferencedLocalVarIds.push(localVarId);
699
+ }
700
+ }
680
701
  const conflictIdsToReplace = Object.create(null);
681
702
  const conflictNamesToReplace = Object.create(null);
682
- // When a dangling reference triggers creation of a new stage variable, remember
683
- // the original (pre-bump) name so subsequent dangling references with the same
684
- // original name and type coalesce to the same stage variable instead of creating
685
- // a second one. Scratchers who pasted scripts referencing what they called
686
- // "score" twice almost certainly meant one variable, not two.
687
- const createdForOriginalName = Object.create(null);
688
- const originalNameKey = (name, type) => `${type}\u0000${name}`;
689
703
 
690
704
  // Cache the list of all variable names by type so that we don't need to
691
705
  // re-calculate this in every iteration of the following loop.
@@ -698,92 +712,77 @@ class Target extends EventEmitter {
698
712
  };
699
713
 
700
714
  for (const varId in allReferences) {
701
- const existing = this.lookupVariableById(varId);
702
- if (existing) {
703
- // The id resolves to a real variable. Normalize displayed names so
704
- // every block field shows the variable's current name. A previous
705
- // target's pass on this load may have created the stage variable with
706
- // a bumped name; refs on this target that still show the original
707
- // name need to be brought into agreement. Scan all refs (not just
708
- // the first) so an inconsistent set of refs to the same id heals
709
- // even when the first ref happens to already match.
710
- if (!conflictNamesToReplace[varId]) {
711
- const staleRef = allReferences[varId].find(
712
- ref => ref.referencingField.value !== existing.name
713
- );
714
- if (staleRef) {
715
- conflictNamesToReplace[varId] = existing.name;
716
- log.warn(
717
- `Reconciled stale displayed name on '${this.getName()}': updated to ` +
718
- `'${existing.name}' for id '${varId}' ` +
719
- `(was '${staleRef.referencingField.value}').`
720
- );
721
- }
722
- }
723
- continue;
724
- }
725
- // The referenced id is not defined anywhere. Treat this as a reference
726
- // to a global from a different project (or from a backpack paste / sprite
727
- // import that lost its definition). Look for a same-name same-type global
728
- // on the stage; if found, queue an id remap, otherwise create a fresh one.
715
+ // We don't care about which var ref we get, they should all have the same var info
729
716
  const varRef = allReferences[varId][0];
730
717
  const varName = varRef.referencingField.value;
731
718
  const varType = varRef.type;
732
- const existingVar = stage.lookupVariableByNameAndType(varName, varType);
733
- if (existingVar) {
734
- if (!conflictIdsToReplace[varId]) {
735
- conflictIdsToReplace[varId] = existingVar.id;
736
- log.warn(
737
- `Reconciled dangling reference on '${this.getName()}': remapped id '${varId}' ` +
738
- `(name '${varName}', type '${varType}') to existing stage variable '${existingVar.id}'.`
739
- );
719
+ if (this.lookupVariableById(varId)) {
720
+ // Found a variable with the id in either the target or the stage,
721
+ // figure out which one.
722
+ if (Object.prototype.hasOwnProperty.call(this.variables, varId)) {
723
+ // If the target has the variable, then check whether the stage
724
+ // has one with the same name and type. If it does, then rename
725
+ // this target specific variable so that there is a distinction.
726
+ const newVarName = renameConflictingLocalVar(varId, varName, varType);
727
+
728
+ if (newVarName) {
729
+ // We are not calling this.blocks.updateBlocksAfterVarRename
730
+ // here because it will search through all the blocks. We already
731
+ // have access to all the references for this var id.
732
+ allReferences[varId].map(ref => {
733
+ ref.referencingField.value = newVarName;
734
+ return ref;
735
+ });
736
+ }
740
737
  }
741
738
  } else {
742
- const coalesceKey = originalNameKey(varName, varType);
743
- const earlierCreated = createdForOriginalName[coalesceKey];
744
- if (earlierCreated) {
745
- // An earlier dangling reference in this pass already triggered creation
746
- // for this original name and type. Coalesce to that stage variable, and
747
- // update this reference's displayed name to match the bumped name so the
748
- // two blocks display consistently.
739
+ // We didn't find the referenced variable id anywhere,
740
+ // Treat it as a reference to a global variable (from the original
741
+ // project this sprite was exported from).
742
+ // Check for whether a global variable of the same name and type exists,
743
+ // and if so, track it to merge with the existing global in a second pass of the blocks.
744
+ const existingVar = stage.lookupVariableByNameAndType(varName, varType);
745
+ if (existingVar) {
749
746
  if (!conflictIdsToReplace[varId]) {
750
- conflictIdsToReplace[varId] = earlierCreated.id;
751
- conflictNamesToReplace[varId] = earlierCreated.freshName;
752
- log.warn(
753
- `Reconciled dangling reference on '${this.getName()}': coalesced id '${varId}' ` +
754
- `(name '${varName}', type '${varType}') with earlier-created stage variable ` +
755
- `'${earlierCreated.id}' (name '${earlierCreated.freshName}').`
756
- );
747
+ conflictIdsToReplace[varId] = existingVar.id;
757
748
  }
758
749
  } else {
750
+ // A global variable with the same name did not already exist,
751
+ // create a new one such that it does not conflict with any
752
+ // names of local variables of the same type.
759
753
  const allNames = allVarNames(varType);
760
754
  const freshName = StringUtil.unusedName(varName, allNames);
761
755
  stage.createVariable(varId, freshName, varType);
762
- // Track the new name so unusedName accounts for it on subsequent calls,
763
- // and remember which stage variable served this original name so
764
- // future same-name dangling references coalesce instead of duplicating.
765
- allNames.push(freshName);
766
- createdForOriginalName[coalesceKey] = {id: varId, freshName};
767
756
  if (!conflictNamesToReplace[varId]) {
768
757
  conflictNamesToReplace[varId] = freshName;
769
- log.warn(
770
- `Reconciled dangling reference on '${this.getName()}': created stage variable ` +
771
- `'${varId}' (name '${freshName}', type '${varType}').`
772
- );
773
758
  }
774
759
  }
775
760
  }
776
761
  }
777
-
778
- // Apply queued id remaps (merge the dangling references with the existing global).
762
+ // Rename any local variables that were missed above because they aren't
763
+ // referenced by any blocks
764
+ for (const id in unreferencedLocalVarIds) {
765
+ const varId = unreferencedLocalVarIds[id];
766
+ const name = this.variables[varId].name;
767
+ const type = this.variables[varId].type;
768
+ renameConflictingLocalVar(varId, name, type);
769
+ }
770
+ // Handle global var conflicts with existing global vars (e.g. a sprite is uploaded, and has
771
+ // blocks referencing some variable that the sprite does not own, and this
772
+ // variable conflicts with a global var)
773
+ // In this case, we want to merge the new variable referenes with the
774
+ // existing global variable
779
775
  for (const conflictId in conflictIdsToReplace) {
780
776
  const existingId = conflictIdsToReplace[conflictId];
781
777
  const referencesToUpdate = allReferences[conflictId];
782
778
  this.mergeVariables(conflictId, existingId, referencesToUpdate);
783
779
  }
784
780
 
785
- // Apply queued field-name updates for newly-created globals whose name was
786
- // bumped to avoid a collision.
781
+ // Handle global var conflicts existing local vars (e.g a sprite is uploaded,
782
+ // and has blocks referencing some variable that the sprite does not own, and this
783
+ // variable conflcits with another sprite's local var).
784
+ // In this case, we want to go through the variable references and update
785
+ // the name of the variable in that reference.
787
786
  for (const conflictId in conflictNamesToReplace) {
788
787
  const newName = conflictNamesToReplace[conflictId];
789
788
  const referencesToUpdate = allReferences[conflictId];
@@ -794,66 +793,6 @@ class Target extends EventEmitter {
794
793
  }
795
794
  }
796
795
 
797
- /**
798
- * Reconciles missing definitions (via `reconcileVariableReferences`) and then
799
- * renames sprite-local variables that name-collide with stage globals so they
800
- * remain distinguishable after import.
801
- *
802
- * Used when importing a sprite into an existing project and when pasting
803
- * blocks from the backpack. Project load uses `reconcileVariableReferences`
804
- * directly so that legitimately-existing local-vs-global name collisions in
805
- * a saved project are not renamed.
806
- */
807
- fixUpVariableReferences () {
808
- if (!this.runtime) return;
809
- const stage = this.runtime.getTargetForStage();
810
- if (!stage || !stage.variables) return;
811
-
812
- // First, ensure every referenced variable, list, or broadcast has a definition.
813
- this.reconcileVariableReferences();
814
-
815
- // The stage's variables are the global scope; there's no local-vs-global
816
- // distinction to disambiguate.
817
- if (this.isStage) return;
818
-
819
- const renameConflictingLocalVar = (id, name, type) => {
820
- const conflict = stage.lookupVariableByNameAndType(name, type);
821
- if (conflict) {
822
- const newName = StringUtil.unusedName(
823
- `${this.getName()}: ${name}`,
824
- this.getAllVariableNamesInScopeByType(type));
825
- this.renameVariable(id, newName);
826
- return newName;
827
- }
828
- return null;
829
- };
830
-
831
- // Walk the references again and rename any sprite-local variables whose
832
- // name and type collide with a stage global. References on this target
833
- // that point at the local are updated to use the new name.
834
- const allReferences = this.blocks.getAllVariableAndListReferences(null, true);
835
- for (const varId in allReferences) {
836
- if (!Object.prototype.hasOwnProperty.call(this.variables, varId)) continue;
837
- const varRef = allReferences[varId][0];
838
- const newVarName = renameConflictingLocalVar(varId, varRef.referencingField.value, varRef.type);
839
- if (newVarName) {
840
- allReferences[varId].map(ref => {
841
- ref.referencingField.value = newVarName;
842
- return ref;
843
- });
844
- }
845
- }
846
-
847
- // Rename any local variables that aren't referenced by any block but still
848
- // collide with a stage global, so the sprite remains internally consistent.
849
- for (const localVarId in this.variables) {
850
- if (!Object.prototype.hasOwnProperty.call(this.variables, localVarId)) continue;
851
- if (allReferences[localVarId]) continue;
852
- const v = this.variables[localVarId];
853
- renameConflictingLocalVar(localVarId, v.name, v.type);
854
- }
855
- }
856
-
857
796
  }
858
797
 
859
798
  module.exports = Target;
@@ -560,13 +560,7 @@ class VirtualMachine extends EventEmitter {
560
560
  this.editingTarget = targets[0];
561
561
  }
562
562
 
563
- if (wholeProject) {
564
- // A loaded project may carry dangling variable, list, or broadcast
565
- // references baked in by historical bugs. Reconcile each target so
566
- // those references resolve cleanly without renaming any legitimate
567
- // local-vs-global name collisions.
568
- targets.forEach(target => target.reconcileVariableReferences());
569
- } else {
563
+ if (!wholeProject) {
570
564
  this.editingTarget.fixUpVariableReferences();
571
565
  }
572
566
 
@@ -1286,12 +1280,6 @@ class VirtualMachine extends EventEmitter {
1286
1280
  target.blocks.createBlock(block);
1287
1281
  });
1288
1282
  target.blocks.updateTargetSpecificBlocks(target.isStage);
1289
- if (!optFromTargetId) {
1290
- // No source target means the blocks come from outside the project (e.g. the
1291
- // backpack). Reconcile any variable, list, or broadcast references against
1292
- // what's defined in the project, creating missing definitions on the stage.
1293
- target.fixUpVariableReferences();
1294
- }
1295
1283
  });
1296
1284
  }
1297
1285